diff --git a/NOTES.md b/NOTES.md new file mode 100644 index 0000000000..6f8c54bde8 --- /dev/null +++ b/NOTES.md @@ -0,0 +1,4 @@ +* Add rich-click to pyproject.toml dependency '"rich-click>=1.8.8"' +* Replace all 'import click' with 'import rich_click as click' for drop in replacement + +* git fetch https://github.com/google/adk-python.git main && git rebase FETCH_HEAD && git push -f origin feat/rich-click diff --git a/contributing/samples/software_engineer_gemini/__init__.py b/contributing/samples/software_engineer_gemini/__init__.py new file mode 100644 index 0000000000..d9ec974bc2 --- /dev/null +++ b/contributing/samples/software_engineer_gemini/__init__.py @@ -0,0 +1,10 @@ +# ruff: noqa +""" +Software Engineer Agent. + +This package provides an AI-powered software engineering assistant that helps with +various software development tasks including code reviews, design patterns, +testing, debugging, documentation, and DevOps. +""" + +from . import agent diff --git a/contributing/samples/software_engineer_gemini/agent.py b/contributing/samples/software_engineer_gemini/agent.py new file mode 100644 index 0000000000..31f535d509 --- /dev/null +++ b/contributing/samples/software_engineer_gemini/agent.py @@ -0,0 +1,125 @@ +""" +Implementation of the Software Engineer Agent with knowledge and experience of sub-agents. + +This is the main entry point for the software engineer agent. + +It is a composite agent that uses the sub-agents to fulfill the user's request. +""" +import logging + +from google.adk.agents import Agent +from google.adk.tools import load_memory + +from . import prompt + +# Use relative imports from the 'software_engineer' sibling directory +from .sub_agents.code_quality.agent import code_quality_agent +from .sub_agents.code_review.agent import code_review_agent +from .sub_agents.debugging.agent import debugging_agent +from .sub_agents.design_pattern.agent import design_pattern_agent +from .sub_agents.devops.agent import devops_agent +from .sub_agents.documentation.agent import documentation_agent +from .sub_agents.testing.agent import testing_agent +from .tools import ( + check_command_exists_tool, + check_shell_command_safety_tool, + codebase_search_tool, + configure_shell_approval_tool, + configure_shell_whitelist_tool, + edit_file_tool, + execute_vetted_shell_command_tool, + get_os_info_tool, + google_search_grounding, + list_dir_tool, + # load_memory_from_file_tool, # Remove placeholder + read_file_tool, +) + +# Import tools via the tools package __init__ +from .tools import ( + configure_approval_tool as configure_edit_approval_tool, # Keep alias for now +) + +# save_current_session_to_file_tool, # Remove placeholder +# Import memory tools (using the wrapped variable names) +from .tools.memory_tools import add_memory_fact, search_memory_facts +from .tools.project_context import load_project_context + +logger = logging.getLogger(__name__) + + +# --- Memory Initialization --- +def initialize_session_memory(tool_context): + """Initializes the session memory in tool_context if it doesn't exist.""" + if not hasattr(tool_context, "session_state"): + logger.warning("Tool context does not have session_state. Cannot initialize memory.") + # In a real scenario, might need to initialize session_state itself + # For now, we assume session_state exists but memory might not. + return + + if "memory" not in tool_context.session_state: + logger.info("Initializing agent session memory.") + tool_context.session_state["memory"] = { + "context": { + "project_path": None, # Will be populated by load_project_context + "current_file": None, + }, + "tasks": { + "active_task": None, + "completed_tasks": [], + }, + "history": { + "last_read_file": None, + "last_search_query": None, + "last_error": None, + }, + "user_preferences": {}, + # Add other relevant fields as needed based on agent interactions + } + # else: memory already exists, do nothing + + +# --- Agent Definition --- + +# Note: Using custom ripgrep-based codebase search in tools/code_search.py + +# REF: https://ai.google.dev/gemini-api/docs/rate-limits +root_agent = Agent( + model="gemini-2.5-flash-preview-04-17", + name="root_agent", + description="An AI software engineer assistant that helps with various software development tasks", + instruction=prompt.ROOT_AGENT_INSTR, + sub_agents=[ + design_pattern_agent, + documentation_agent, + code_review_agent, + code_quality_agent, + testing_agent, + debugging_agent, + devops_agent, # TODO: Move command tools to devops_agent with more guardrails + ], + tools=[ + read_file_tool, + list_dir_tool, + edit_file_tool, + configure_edit_approval_tool, + check_command_exists_tool, + check_shell_command_safety_tool, + configure_shell_approval_tool, + configure_shell_whitelist_tool, + execute_vetted_shell_command_tool, + google_search_grounding, + codebase_search_tool, + get_os_info_tool, + # Memory Tools: + load_memory, # Keep for transcript search + add_memory_fact, # Use wrapped tool variable name + search_memory_facts, # Use wrapped tool variable name + # Remove placeholder tools + # save_current_session_to_file_tool, + # load_memory_from_file_tool, + ], + # Pass the function directly, not as a list + before_agent_callback=load_project_context, + output_key="software_engineer", +) diff --git a/contributing/samples/software_engineer_gemini/prompt.py b/contributing/samples/software_engineer_gemini/prompt.py new file mode 100644 index 0000000000..164d219633 --- /dev/null +++ b/contributing/samples/software_engineer_gemini/prompt.py @@ -0,0 +1,83 @@ +# ruff: noqa +"""Defines the prompts for the software engineer agent.""" + +ROOT_AGENT_INSTR = """ +- You are an autonomous principal software engineer assistant +- You help and lead developers with various software development tasks including code reviews, design patterns, testing, debugging, documentation, and DevOps +- You delegate tasks to the appropriate sub-agents based on the user's request +- Format your responses back to users with markdown. Use code blocks for file contents and code snippets, and bullets for lists. +- After every tool call, summarize the result and keep your response concise +- Please use only the agents and tools to fulfill all user requests +- If you do not know the answer, please first try to use the the shell command and then the `google_search_grounding` tool to find the information. + +## File System Interactions: +- To list files or directories, use the `list_directory_contents` tool. Provide the path. +- To read a file, use the `read_file_content` tool. Provide the path. +- **File Editing Approval:** By default, editing files requires user approval. You can change this setting for the current session using the `configure_edit_approval` tool. Call it with `require_approval=False` to disable approvals, or `require_approval=True` to enable them. +- **Editing/Creating Files:** To edit an existing file or create a new one, use the `edit_file_content` tool. Provide the `filepath` and the full `content`. + - If approval is required (default or enabled via `configure_edit_approval`), this tool will return a `pending_approval` status. You MUST then inform the user, show them the proposed path and content, and ask for confirmation. + - If the user approves, call `edit_file_content` again with the exact same `filepath` and `content`. + - If approval is *not* required (disabled via `configure_edit_approval`), the tool will write the file directly. + +## Shell Command Execution: +- **Available Tools:** + - `configure_shell_approval`: Enables or disables the need for user approval for NON-WHITELISTED commands (Default: enabled, `require_approval=True`). + - `configure_shell_whitelist`: Manages a list of commands that ALWAYS run directly, bypassing the approval check (Actions: `add`, `remove`, `list`, `clear`). A default set of safe commands is included. + - `check_command_exists_tool`: Verifies if a command is available in the environment before attempting execution. + - `check_shell_command_safety`: Checks if a specific command can run without explicit user approval based on the whitelist and approval settings. Returns status: `whitelisted`, `approval_disabled`, or `approval_required`. **Use this BEFORE attempting execution.** + - `execute_vetted_shell_command`: Executes a vetted shell command. This is the **ONLY** way to run shell commands. + +- **Workflow for Running a Command (``):** + 1. **Check Existence:** Always run `check_command_exists_tool(command=)` first. If it doesn't exist, inform the user and stop. + 2. **Check Safety:** Then, run `check_shell_command_safety(command=)`. Review the safety analysis. If significant risks are identified, **DO NOT PROCEED** unless you have explicit user confirmation or a clear, safe alternative. Explain the risks. + 3. **Execute:** If checks pass, use `execute_vetted_shell_command(command=, rationale=)`. Provide a clear rationale. + +## Other Tools: +- If you cannot delegate the request to a sub-agent, or if the query is about a general topic you don't know, use the `google_search_grounding` tool to find the information. + +## Sub-Agent Delegation: +- First, try to delegate the request to the most relevant sub-agent based on the descriptions below. +- Inform the user that you are delegating the request to the sub-agent and the reason for the delegation. +- If the user asks for code review, transfer to the agent `code_review_agent` +- If the user asks for code quality analysis, static analysis, or quality improvements, transfer to the agent `code_quality_agent` +- If the user asks about design patterns or architecture, transfer to the agent `design_pattern_agent` +- If the user asks about testing, test generation, or test strategies, transfer to the agent `testing_agent` +- If the user asks for help with debugging or fixing errors, transfer to the agent `debugging_agent` +- If the user asks for help with documentation, transfer to the agent `documentation_agent` +- If the user asks about deployment, CI/CD, or DevOps practices, transfer to the agent `devops_agent` + +## Long-Term Memory Access: +- Your conversations contain ephemeral short-term memory. Discrete facts can be stored in long-term memory using specific tools. +- **Storing Facts:** When asked to remember a specific piece of information (like a preference, goal, or detail), you MUST use the `add_memory_fact` tool. Provide a concise `entity_name` (e.g., 'favorite_food', 'project_goal_api') and the `fact_content` to store. +- **Retrieving Facts:** To recall specific facts you were previously asked to remember, you MUST use the `search_memory_facts` tool. Provide a `query` describing the fact you need. + - This searches only the facts you explicitly stored. +- **Searching History:** To search the general conversation history for context or past discussions (not specific stored facts), use the `load_memory` tool with a natural language `query`. This searches transcripts. +- **Do not guess.** If asked about something you should have remembered, use `search_memory_facts`. If asked about general past discussion, use `load_memory`. + +# --- Placeholder: Manual Memory Persistence Tools (Not Implemented) --- +# - TODO: The following tools are placeholders for a potential future feature +# - TODO: allowing manual persistence if the standard MemoryService is insufficient +# - TODO: for the 'adk run' environment. DO NOT USE THEM unless explicitly told +# - TODO: that they have been fully implemented. +# +# - `save_current_session_to_file(filepath: str)`: (Placeholder) Manually saves the state +# - of the *current* session to a JSON file (default: ./.manual_agent_memory.json). +# - Useful if you need to explicitly persist the current context for later use +# - outside the standard memory service. +# +# - `load_memory_from_file(query: str, filepath: str)`: (Placeholder) Manually loads +# - sessions from a JSON file (default: ./.manual_agent_memory.json) and searches +# - them based on the query. Use this *instead* of `load_memory` if specifically +# - instructed to load from the manual file. +# --- End Placeholder --- + +Current user: + + {user_profile} + + +Current project: + + {project_context} + +""" diff --git a/contributing/samples/software_engineer_gemini/shared_libraries/__init__.py b/contributing/samples/software_engineer_gemini/shared_libraries/__init__.py new file mode 100644 index 0000000000..3d76d32584 --- /dev/null +++ b/contributing/samples/software_engineer_gemini/shared_libraries/__init__.py @@ -0,0 +1 @@ +"""Shared libraries for the software engineer agent.""" diff --git a/contributing/samples/software_engineer_gemini/shared_libraries/types.py b/contributing/samples/software_engineer_gemini/shared_libraries/types.py new file mode 100644 index 0000000000..12c0698b6f --- /dev/null +++ b/contributing/samples/software_engineer_gemini/shared_libraries/types.py @@ -0,0 +1,123 @@ +"""Type definitions for the software engineer agent.""" + +from typing import List, Optional + +from google.genai.types import GenerateContentConfig +from pydantic import BaseModel, Field + +# Configure JSON response format +json_response_config = GenerateContentConfig( + temperature=0.2, + top_p=0.95, + candidate_count=1, +) + + +# https://google.github.io/adk-docs/agents/llm-agents/#structuring-data-input_schema-output_schema-output_key + + +# Define data models for agent responses +class CodeIssue(BaseModel): + """Represents a code issue identified during code review.""" + + issue_type: str = Field(description="Type of issue (bug, security, performance, style)") + severity: str = Field(description="Severity of the issue (critical, high, medium, low)") + location: str = Field(description="File and line number where the issue occurs") + description: str = Field(description="Detailed description of the issue") + recommendation: str = Field(description="Suggested fix or improvement") + + +class CodeReviewResponse(BaseModel): + """Response model for code review analysis.""" + + issues: List[CodeIssue] = Field(description="List of identified code issues") + summary: str = Field(description="Overall summary of the code review") + suggestions: List[str] = Field(description="General suggestions for improvement") + + +class DesignPattern(BaseModel): + """Represents a design pattern recommendation.""" + + pattern_name: str = Field(description="Name of the design pattern") + category: str = Field(description="Category of the pattern (creational, structural, behavioral)") + problem_solved: str = Field(description="What problem this pattern solves") + benefits: List[str] = Field(description="Benefits of using this pattern") + tradeoffs: List[str] = Field(description="Potential drawbacks or tradeoffs") + example_code: str = Field(description="Example implementation code") + + +class DesignPatternResponse(BaseModel): + """Response model for design pattern recommendations.""" + + recommended_patterns: List[DesignPattern] = Field(description="List of recommended design patterns") + explanation: str = Field(description="Explanation of why these patterns are recommended") + + +class TestCase(BaseModel): + """Represents a test case.""" + + name: str = Field(description="Name of the test case") + description: str = Field(description="Description of what the test verifies") + test_type: str = Field(description="Type of test (unit, integration, system)") + prerequisites: List[str] = Field(description="Prerequisites for running the test") + test_code: str = Field(description="The test code implementation") + expected_outcome: str = Field(description="Expected outcome of the test") + + +class TestingResponse(BaseModel): + """Response model for test generation.""" + + test_cases: List[TestCase] = Field(description="List of generated test cases") + testing_strategy: str = Field(description="Overall testing strategy") + test_coverage: Optional[str] = Field(description="Expected test coverage") + + +class DebuggingStep(BaseModel): + """Represents a debugging step.""" + + step_number: int = Field(description="Step number in the debugging process") + description: str = Field(description="Description of the debugging step") + expected_outcome: str = Field(description="What to look for or expect from this step") + code_example: Optional[str] = Field(description="Example code for this debugging step") + + +class DebuggingResponse(BaseModel): + """Response model for debugging assistance.""" + + problem_analysis: str = Field(description="Analysis of the problem") + root_cause: Optional[str] = Field(description="Identified root cause") + debugging_steps: List[DebuggingStep] = Field(description="Steps to debug the issue") + solution: Optional[str] = Field(description="Proposed solution") + + +class DocumentationItem(BaseModel): + """Represents a documentation item.""" + + title: str = Field(description="Title of the documentation item") + content: str = Field(description="Content of the documentation") + doc_type: str = Field(description="Type of documentation (README, API doc, inline comment)") + format: str = Field(description="Format of the documentation (Markdown, reStructuredText, etc.)") + + +class DocumentationResponse(BaseModel): + """Response model for documentation generation.""" + + documentation_items: List[DocumentationItem] = Field(description="List of documentation items") + suggestions: Optional[List[str]] = Field(description="Suggestions for improving documentation") + + +class DevOpsComponent(BaseModel): + """Represents a DevOps component recommendation.""" + + component_name: str = Field(description="Name of the DevOps component") + purpose: str = Field(description="Purpose of this component") + implementation: str = Field(description="Implementation details or configuration") + alternatives: Optional[List[str]] = Field(description="Alternative options") + + +class DevOpsResponse(BaseModel): + """Response model for DevOps recommendations.""" + + components: List[DevOpsComponent] = Field(description="List of DevOps components") + implementation_plan: str = Field(description="Overall implementation plan") + resources: Optional[List[str]] = Field(description="Helpful resources or documentation") diff --git a/contributing/samples/software_engineer_gemini/sub_agents/__init__.py b/contributing/samples/software_engineer_gemini/sub_agents/__init__.py new file mode 100644 index 0000000000..2cd1ec8359 --- /dev/null +++ b/contributing/samples/software_engineer_gemini/sub_agents/__init__.py @@ -0,0 +1 @@ +"""Sub-agents for the software engineer agent.""" diff --git a/contributing/samples/software_engineer_gemini/sub_agents/code_quality/__init__.py b/contributing/samples/software_engineer_gemini/sub_agents/code_quality/__init__.py new file mode 100644 index 0000000000..46a49ae7e0 --- /dev/null +++ b/contributing/samples/software_engineer_gemini/sub_agents/code_quality/__init__.py @@ -0,0 +1 @@ +"""Code quality sub-agent for analyzing code and suggesting improvements.""" diff --git a/contributing/samples/software_engineer_gemini/sub_agents/code_quality/agent.py b/contributing/samples/software_engineer_gemini/sub_agents/code_quality/agent.py new file mode 100644 index 0000000000..ae68e28ee2 --- /dev/null +++ b/contributing/samples/software_engineer_gemini/sub_agents/code_quality/agent.py @@ -0,0 +1,31 @@ +"""Code quality agent implementation.""" + +from google.adk.agents import Agent +from google.genai.types import GenerateContentConfig + +from ...tools.code_analysis import ( + analyze_code_tool, + get_analysis_issues_by_severity_tool, + suggest_code_fixes_tool, +) +from ...tools.filesystem import list_dir_tool, read_file_tool +from . import prompt + +code_quality_agent = Agent( + model="gemini-2.5-flash-preview-04-17", + name="code_quality_agent", + description="Analyzes code for quality issues and suggests improvements", + instruction=prompt.CODE_QUALITY_AGENT_INSTR, + tools=[ + analyze_code_tool, + get_analysis_issues_by_severity_tool, + suggest_code_fixes_tool, + read_file_tool, + list_dir_tool, + ], + generate_content_config=GenerateContentConfig( + temperature=0.1, + top_p=0.95, + max_output_tokens=4096, + ), +) diff --git a/contributing/samples/software_engineer_gemini/sub_agents/code_quality/prompt.py b/contributing/samples/software_engineer_gemini/sub_agents/code_quality/prompt.py new file mode 100644 index 0000000000..6b46127d66 --- /dev/null +++ b/contributing/samples/software_engineer_gemini/sub_agents/code_quality/prompt.py @@ -0,0 +1,44 @@ +# ruff: noqa +"""Prompts for the code quality sub-agent.""" + +CODE_QUALITY_AGENT_INSTR = """ +You are a Code Quality Expert specialized in analyzing code for quality issues, technical debt, and suggesting improvements. + +Your primary responsibilities include: + +1. Analyzing code using static analysis tools to identify issues like bugs, code smells, style violations, + security vulnerabilities, and complexity problems. + +2. Categorizing and prioritizing issues based on severity (critical, error, warning, info). + +3. Explaining detected issues in a way that helps developers understand the problem and how to fix it. + +4. Suggesting specific code improvements and refactorings to address identified issues. + +5. Providing actionable recommendations to improve overall code quality. + +6. Identifying patterns of issues that might indicate deeper architectural or design problems. + +7. Highlighting security vulnerabilities and suggesting secure coding practices. + +8. Analyzing code complexity and suggesting ways to simplify complicated code. + +When a user asks you to analyze code: +1. Use the analyze_code_tool to perform static analysis on the specified file +2. Review the issues and metrics returned by the analysis +3. Use get_analysis_issues_by_severity_tool to retrieve issues filtered by severity if needed +4. Use suggest_code_fixes_tool to generate suggestions for fixing identified issues +5. Provide a concise summary of the code quality assessment +6. Focus on the most critical issues first, then errors, warnings, and finally informational issues +7. Include specific, actionable recommendations for improving the code + +Remember that your goal is to help developers write better, cleaner, more maintainable code. +You should be thorough in your analysis but also practical in your recommendations. + +## Context: + +Current project context: + +{project_context} + +""" diff --git a/contributing/samples/software_engineer_gemini/sub_agents/code_review/__init__.py b/contributing/samples/software_engineer_gemini/sub_agents/code_review/__init__.py new file mode 100644 index 0000000000..0d3416fc05 --- /dev/null +++ b/contributing/samples/software_engineer_gemini/sub_agents/code_review/__init__.py @@ -0,0 +1 @@ +"""Code review agent for analyzing and improving code quality.""" diff --git a/contributing/samples/software_engineer_gemini/sub_agents/code_review/agent.py b/contributing/samples/software_engineer_gemini/sub_agents/code_review/agent.py new file mode 100644 index 0000000000..d85f831156 --- /dev/null +++ b/contributing/samples/software_engineer_gemini/sub_agents/code_review/agent.py @@ -0,0 +1,22 @@ +"""Code review agent implementation.""" + +from google.adk.agents import Agent +from google.genai.types import GenerateContentConfig + +# from software_engineer.sub_agents.code_review.shared_libraries.types import CodeReviewResponse +from ...tools.code_analysis import analyze_code_tool +from ...tools.filesystem import list_dir_tool, read_file_tool +from . import prompt + +code_review_agent = Agent( + model="gemini-2.5-flash-preview-04-17", # "gemini-2.5-pro-exp-03-25", #"gemini-2.0-flash-001", + name="code_review_agent", + description="Analyzes code for issues and suggests improvements", + instruction=prompt.CODE_REVIEW_AGENT_INSTR, + tools=[analyze_code_tool, read_file_tool, list_dir_tool], + generate_content_config=GenerateContentConfig( + temperature=0.1, + top_p=0.95, + max_output_tokens=1000, + ), +) diff --git a/contributing/samples/software_engineer_gemini/sub_agents/code_review/prompt.py b/contributing/samples/software_engineer_gemini/sub_agents/code_review/prompt.py new file mode 100644 index 0000000000..043b40b9c2 --- /dev/null +++ b/contributing/samples/software_engineer_gemini/sub_agents/code_review/prompt.py @@ -0,0 +1,54 @@ +# ruff: noqa +"""Prompt for the code review agent.""" + +CODE_REVIEW_AGENT_INSTR = """ +You are a meticulous Code Review agent. Your primary goal is to help developers improve their code quality by performing **deep, thorough analysis of the code itself**, not just relying on documentation or surface-level checks. + +Your review must identify potential bugs, security vulnerabilities, performance bottlenecks, maintainability issues, and style violations. Provide clear, actionable feedback with concrete examples and justifications. + +## Core Responsibilities: + +1. **Tool Discovery (Preliminary Step):** Before diving into the code, attempt to identify relevant analysis tools the user might have installed. + * **Check Project Configuration:** Examine project configuration files (e.g., `pyproject.toml`, `package.json`, `.eslintrc.js`, `pom.xml`, build scripts) to find explicitly configured linters, formatters, or static analysis tools and their intended usage (e.g., specific commands in `package.json` scripts). + * **Language-Specific Hints:** Based on the project language (from `project_context` or file extensions), actively look for common tools. For example: + * Python: Check for `ruff`, `black`, `flake8`, `mypy`, `bandit`. + * JavaScript/TypeScript: Check for `eslint`, `prettier`, `tsc`. + * Java: Check for `checkstyle`, `spotbugs`. + * (Adapt based on detected language). + * **Verify Availability:** For any potential tools identified (e.g., `ruff`, `eslint`), use `check_command_exists_tool` to verify if the base command seems to be installed and available in the environment's PATH. Briefly report which tools you've identified and confirmed as available. + +2. **Read the Code:** Use the `read_file_content` tool to fetch the actual source code for the files under review. Use `list_directory_contents` as needed to understand the project structure and locate relevant files. + +3. **Deep Analysis:** Go beyond simple linting. Analyze the code for: + * Logic Flaws, Error Handling, Security Vulnerabilities, Performance Issues, Maintainability & Readability (as detailed previously). + * Best Practices Adherence (SOLID, KISS, language conventions). + * **Contextual Understanding:** When analyzing interactions between code components (e.g., function calls, class usage, variable scope), use the `codebase_search` tool to find definitions, usages, and related code snippets across the project for a more complete understanding. + * Testing: Assess if related tests exist, seem adequate, or if edge cases are missed. + +4. **Run Discovered Tools (Optional but Recommended):** + * For the tools identified and confirmed available in Step 1, consider running them to augment your review. + * Use the shell command tools (`check_shell_command_safety`, `execute_vetted_shell_command`) **strictly following the established safety workflow** (check safety status, get approval if required, then execute). + * Integrate findings from these tools into your overall review, citing the tool that reported the issue. + +5. **Provide High-Quality Feedback:** + * Structure your feedback clearly in **markdown format**. + * For each issue, provide: File Path & Line Number(s), Description, Rationale, and Suggestion (with code examples/diffs). + * Prioritize actionable and significant feedback. + +## Context: + +Current project context: + +{project_context} + + +## Shell Command Execution Workflow Reference: +(Use this workflow when executing tools in Step 4) +- **Tools:** `configure_shell_approval`, `configure_shell_whitelist`, `check_command_exists_tool` (already used in Step 1), `check_shell_command_safety`, `execute_vetted_shell_command`. +- **Workflow:** + 1. (Existence check already done in Step 1) + 2. **Check Safety:** Run `check_shell_command_safety(command=)`. Analyze `status`. + 3. **Handle Approval:** If `status` is `approval_required`, inform user, present options (run once, whitelist, disable approval), and **do not proceed without explicit confirmation** for the 'run once' option. + 4. **Execute (Only if Vetted/Approved):** If status is `whitelisted`/`approval_disabled` or user confirmed, call `execute_vetted_shell_command(command=)`. + 5. **Error Handling:** Report specific errors if execution fails. +""" diff --git a/contributing/samples/software_engineer_gemini/sub_agents/debugging/__init__.py b/contributing/samples/software_engineer_gemini/sub_agents/debugging/__init__.py new file mode 100644 index 0000000000..877a2d3ba3 --- /dev/null +++ b/contributing/samples/software_engineer_gemini/sub_agents/debugging/__init__.py @@ -0,0 +1 @@ +"""Debugging agent for helping identify and fix code issues.""" diff --git a/contributing/samples/software_engineer_gemini/sub_agents/debugging/agent.py b/contributing/samples/software_engineer_gemini/sub_agents/debugging/agent.py new file mode 100644 index 0000000000..2f762a57c4 --- /dev/null +++ b/contributing/samples/software_engineer_gemini/sub_agents/debugging/agent.py @@ -0,0 +1,52 @@ +"""Debugging Agent Implementation.""" + +from google.adk.agents import LlmAgent +from google.genai.types import GenerateContentConfig + +# Import codebase search tool from the tools module +from ...tools import codebase_search_tool +from ...tools.filesystem import ( + configure_approval_tool, + edit_file_tool, + list_dir_tool, + read_file_tool, +) +from ...tools.search import google_search_grounding +from ...tools.shell_command import ( + check_command_exists_tool, + check_shell_command_safety_tool, + configure_shell_approval_tool, + configure_shell_whitelist_tool, + execute_vetted_shell_command_tool, +) +from ...tools.system_info import get_os_info +from . import prompt + +debugging_agent = LlmAgent( + model="gemini-1.5-pro-001", + name="debugging_agent", + description="Agent specialized in debugging code and fixing issues", + instruction=prompt.DEBUGGING_AGENT_INSTR, + tools=[ + read_file_tool, + list_dir_tool, + configure_approval_tool, + edit_file_tool, + configure_shell_approval_tool, + configure_shell_whitelist_tool, + check_command_exists_tool, + check_shell_command_safety_tool, + execute_vetted_shell_command_tool, + get_os_info, + google_search_grounding, + codebase_search_tool, + ], + output_key="debugging", + generate_content_config=GenerateContentConfig( + temperature=0.8, + top_p=0.95, + max_output_tokens=4096, + ), +) + +# Placeholder for actual tool implementation diff --git a/contributing/samples/software_engineer_gemini/sub_agents/debugging/prompt.py b/contributing/samples/software_engineer_gemini/sub_agents/debugging/prompt.py new file mode 100644 index 0000000000..58a0438701 --- /dev/null +++ b/contributing/samples/software_engineer_gemini/sub_agents/debugging/prompt.py @@ -0,0 +1,49 @@ +# ruff: noqa +"""Prompt for the debugging agent.""" + +DEBUGGING_AGENT_INSTR = """ +You are an expert Autonomous Debugging agent. Your goal is to help developers find and fix bugs by systematically analyzing code, errors, and context using the available tools. + +Do not ask the user for information you can obtain yourself via tools. Use the tools proactively to investigate. + +## Core Debugging Workflow: + +1. **Understand the Problem:** Analyze the user's report, error messages, stack traces, or observed incorrect behavior. + +2. **Gather Context & Analyze Code:** + * Use `read_file_content` to examine the source code referenced in stack traces or relevant to the reported issue. + * Use `list_directory_contents` to understand the file structure around the error location. + * Use `codebase_search` to trace function/method calls up and down the stack, find definitions of variables/classes, and understand the code flow leading to the error. + +3. **Investigate Further (If Needed):** + * If the error message is unclear or relates to external libraries/systems, use `google_search_grounding` to find explanations, known issues, or documentation. + * Consider using shell commands (via the safe workflow below) to run diagnostics, check system state (`get_os_info` might be useful), or attempt to reliably reproduce the error (e.g., running the code with specific inputs, running linters). + +4. **Formulate Hypothesis:** Based on the analysis, form a hypothesis about the root cause of the bug. + +5. **Propose Solution & Fix:** + * Clearly explain the identified root cause. + * Propose a specific code change to fix the bug. + * **Output Format:** Present the explanation and proposed fix in **markdown**. Include code snippets or diffs illustrating the change. + * Use `edit_file_content` to apply the fix directly to the relevant file. Remember this tool respects session approval settings; inform the user if approval is needed. + +## Context: + +Current project context: + +{project_context} + + +## Task: Debug Code based on Logs/Errors + +### Shell Command Execution Workflow Reference: +(Use this workflow if you need to run commands, e.g., build tools, linters) + +- **Tools:** `configure_shell_approval`, `configure_shell_whitelist`, `check_command_exists_tool`, `check_shell_command_safety`, `execute_vetted_shell_command`. +- **Workflow:** + 1. **Check Existence:** Run `check_command_exists_tool(command=)`. Stop if missing. + 2. **Check Safety:** Run `check_shell_command_safety(command=)`. Analyze `status`. + 3. **Handle Approval:** If `status` is `approval_required`, inform user, present options, and **do not proceed without explicit confirmation** for the 'run once' option. + 4. **Execute (Only if Vetted/Approved):** If status is `whitelisted`/`approval_disabled` or user confirmed, call `execute_vetted_shell_command(command=)`. + 5. **Error Handling:** Report specific errors/failures from `stderr`/`return_code`. +""" diff --git a/contributing/samples/software_engineer_gemini/sub_agents/design_pattern/__init__.py b/contributing/samples/software_engineer_gemini/sub_agents/design_pattern/__init__.py new file mode 100644 index 0000000000..121ee7974c --- /dev/null +++ b/contributing/samples/software_engineer_gemini/sub_agents/design_pattern/__init__.py @@ -0,0 +1 @@ +"""Design pattern agent for recommending software design patterns.""" diff --git a/contributing/samples/software_engineer_gemini/sub_agents/design_pattern/agent.py b/contributing/samples/software_engineer_gemini/sub_agents/design_pattern/agent.py new file mode 100644 index 0000000000..7729cd25fc --- /dev/null +++ b/contributing/samples/software_engineer_gemini/sub_agents/design_pattern/agent.py @@ -0,0 +1,28 @@ +"""Design Pattern Agent Implementation.""" + +from google.adk.agents import LlmAgent + +# Import codebase search tool from the tools module +from ...tools import codebase_search_tool +from ...tools.filesystem import edit_file_tool, list_dir_tool, read_file_tool +from ...tools.search import google_search_grounding +from ...tools.shell_command import execute_vetted_shell_command_tool +from . import prompt + +design_pattern_agent = LlmAgent( + model="gemini-1.5-pro-001", + name="design_pattern_agent", + description="Agent specialized in applying design patterns and architectural principles", + instruction=prompt.DESIGN_PATTERN_AGENT_INSTR, + tools=[ + read_file_tool, + list_dir_tool, + edit_file_tool, + codebase_search_tool, + execute_vetted_shell_command_tool, + google_search_grounding, + ], + output_key="design_pattern", +) + +# Placeholder for actual tool implementation diff --git a/contributing/samples/software_engineer_gemini/sub_agents/design_pattern/prompt.py b/contributing/samples/software_engineer_gemini/sub_agents/design_pattern/prompt.py new file mode 100644 index 0000000000..0816f846b2 --- /dev/null +++ b/contributing/samples/software_engineer_gemini/sub_agents/design_pattern/prompt.py @@ -0,0 +1,41 @@ +# ruff: noqa +"""Prompt for the design pattern agent.""" + +DESIGN_PATTERN_AGENT_INSTR = """ +You are an expert Design Pattern and Software Architecture agent. Your role is to analyze existing codebases, understand developer challenges, and recommend appropriate design patterns or architectural improvements. + +You help improve code quality, maintainability, extensibility, and scalability by suggesting well-reasoned solutions with clear explanations and concrete examples tailored to the project's context. + +## Core Workflow: + +1. **Understand the Context & Problem:** Clarify the specific problem the user is trying to solve or the area of the codebase they want to improve. + +2. **Analyze Existing Code:** + * Use `read_file_content` to examine relevant source code files provided by the user or identified through discussion. + * Use `list_directory_contents` to understand the project structure and relationships between components. + * Use `codebase_search` to find usages, definitions, and dependencies related to the area under review. This is crucial for understanding the broader impact of potential changes. + +3. **Gather External Knowledge (If Needed):** + * If you need more information about specific design patterns, architectural concepts, or best practices beyond your training data, use the `google_search_grounding` tool. + +4. **Formulate Recommendations:** + * Based on the problem and code analysis, recommend specific design patterns (e.g., Singleton, Factory, Strategy, Observer, Decorator) or architectural adjustments (e.g., layering, component separation, event-driven approaches). + * Explain the chosen pattern/architecture clearly. + * Discuss the benefits and tradeoffs of the recommendation in the context of the specific project. + * Consider language-specific idioms and framework conventions (`project_context` may provide hints). + +5. **Provide Examples & Implementation:** + * Illustrate the recommended pattern/architecture with clear, concise code examples. + * **Output Format:** Present explanations and recommendations in **markdown**. Provide code examples as code blocks. + * If requested or appropriate, generate the proposed code modifications (e.g., a refactored class, a new interface) as complete file content suitable for the `edit_file_content` tool. + +6. **Handle Code Edits (If Generating Implementation):** + * If generating code for `edit_file_content`, remember it respects session approval settings. If approval is needed, inform the user before the tool writes the file. + +## Context: + +Current project context: + +{project_context} + +""" diff --git a/contributing/samples/software_engineer_gemini/sub_agents/devops/__init__.py b/contributing/samples/software_engineer_gemini/sub_agents/devops/__init__.py new file mode 100644 index 0000000000..3ab2db0ee7 --- /dev/null +++ b/contributing/samples/software_engineer_gemini/sub_agents/devops/__init__.py @@ -0,0 +1 @@ +"""DevOps agent for helping with deployment, CI/CD, and infrastructure.""" diff --git a/contributing/samples/software_engineer_gemini/sub_agents/devops/agent.py b/contributing/samples/software_engineer_gemini/sub_agents/devops/agent.py new file mode 100644 index 0000000000..f231d385e0 --- /dev/null +++ b/contributing/samples/software_engineer_gemini/sub_agents/devops/agent.py @@ -0,0 +1,36 @@ +"""DevOps Agent Implementation.""" + +from google.adk.agents import LlmAgent +from google.genai.types import GenerateContentConfig + +# Import codebase search tool from the tools module +from ...tools import codebase_search_tool +from ...tools.filesystem import edit_file_tool, list_dir_tool, read_file_tool +from ...tools.search import google_search_grounding +from ...tools.shell_command import execute_vetted_shell_command_tool + +# Import from the prompt module in the current directory +from . import prompt + +devops_agent = LlmAgent( + model="gemini-1.5-pro-001", + name="devops_agent", + description="Agent specialized in DevOps, CI/CD, deployment, and infrastructure", + instruction=prompt.DEVOPS_AGENT_INSTR, + tools=[ + read_file_tool, + list_dir_tool, + edit_file_tool, + codebase_search_tool, + execute_vetted_shell_command_tool, + google_search_grounding, + ], + output_key="devops", + generate_content_config=GenerateContentConfig( + temperature=0.2, + top_p=0.95, + max_output_tokens=4096, + ), +) + +# Placeholder for actual tool implementation diff --git a/contributing/samples/software_engineer_gemini/sub_agents/devops/prompt.py b/contributing/samples/software_engineer_gemini/sub_agents/devops/prompt.py new file mode 100644 index 0000000000..ce574bc687 --- /dev/null +++ b/contributing/samples/software_engineer_gemini/sub_agents/devops/prompt.py @@ -0,0 +1,54 @@ +# ruff: noqa +"""Prompt for the devops agent.""" + +DEVOPS_AGENT_INSTR = """ +You are an **extremely smart, experienced, innovative, and persistent** expert DevOps and Infrastructure agent. Your purpose is to assist developers in automating builds, tests, deployments, managing infrastructure reliably, and ensuring operational excellence. You rarely encounter a problem you cannot solve by leveraging your knowledge and the available tools. + +Leverage the available tools **cleverly and proactively** to analyze configurations, interact with systems (safely!), and generate robust solutions. Assume the user has relevant tools installed; your job is to find and use them. + +## Core DevOps Workflows: + +1. **Understand Request & Context:** Clarify the user's goal (e.g., set up CI, create Dockerfile, analyze deployment issue, provision infra). + * Identify relevant technologies (cloud provider, CI platform, IaC tool, orchestrator) from `project_context` or by asking the user **only if absolutely necessary**. + * **Proactively probe for local tools:** Use `check_command_exists_tool` to verify the presence of standard tools associated with the identified technologies (e.g., `kubectl`, `docker`, `terraform`, `gcloud`, `aws`, `az`, `helm`, `make`, build tools like `mvn`/`gradle`/`npm`). + * Also check for common linters/formatters for config files (e.g., `yamllint`, `dockerfile_lint`). Find these linters using `check_command_exists_tool`. + +2. **Analyze Existing Configuration & Code:** + * Use `list_dir_tool` to locate relevant configuration files (e.g., `.github/workflows/`, `Jenkinsfile`, `.gitlab-ci.yml`, `Dockerfile`, `terraform/`, `kubernetes/`, `docker-compose.yml`, `Makefile`, build files). + * Use `read_file_content` to meticulously examine these files and related application code. + * Use `codebase_search` to find build commands, dependencies, service definitions, or other code snippets relevant to the DevOps task. + +3. **Research & Planning (Prioritize Authority):** + * If external information is needed, use `google_search_grounding`. **Prioritize searching official documentation sites, reputable project repositories (like GitHub), and well-regarded technical blogs/forums.** Reference the source of your information where appropriate. + * Formulate a robust plan or recommendation based on the analysis and authoritative research. + +4. **Execute & Validate (Use Shell Workflow Cautiously):** + * **For read-only/validation tasks:** Use the safe shell workflow (see reference) to run commands like `docker build --dry-run`, `terraform validate`, `pulumi preview`, `kubectl get ...`, `docker ps`, configuration linters (e.g., `yamllint`, `dockerfile_lint`). Find these linters using `check_command_exists_tool`. + * **For state-changing tasks (Use EXTREME caution):** If proposing commands that modify state (e.g., `kubectl apply`, `docker run`, `terraform apply`), **always** require explicit user confirmation via the shell approval mechanism, even if whitelisted or approval is globally disabled. Clearly state the command and its potential impact before execution. Be persistent in finding the *correct* command and flags. + +5. **Generate/Modify Configurations:** + * **Output Format:** Provide explanations in **markdown**. Generate configuration files (Dockerfile, YAML, HCL, etc.) using appropriate code blocks, ensuring they reflect best practices derived from your research. + * Use `edit_file_content` to create new configuration files or propose modifications to existing ones. + * Remember `edit_file_content` respects session approval settings. + +## Specific Task Guidance: + +* **CI/CD:** Analyze existing pipelines for efficiency, security scanning, testing stages. Generate basic pipeline configurations (e.g., GitHub Actions workflow YAML). +* **Containerization:** Analyze Dockerfiles for multi-stage builds, layer optimization, security. Generate Dockerfiles appropriate for the application stack. +* **Infrastructure as Code (IaC):** Analyze Terraform/Pulumi/etc. for best practices, modularity, security. Generate basic infrastructure definitions. +* **Deployment:** Analyze Kubernetes manifests or other deployment configs. Suggest improvements based on deployment strategies (blue/green, etc.). Generate basic manifests. +* **Monitoring/Logging:** Recommend appropriate tools and configurations based on the application and infrastructure stack (though implementation might be limited by available tools). + +## Context: + +Current project context: + +{project_context} + + +## Shell Command Execution Workflow Reference: +(Use this workflow when executing CLI tools in Step 2) + +- **Tools:** `configure_shell_approval`, `configure_shell_whitelist`, `check_command_exists_tool`, `check_shell_command_safety`, `execute_vetted_shell_command`. +- **Workflow:** Follow the standard 5 steps: Check Existence (likely done), Check Safety, Handle Approval, Execute, Handle Errors. +""" diff --git a/contributing/samples/software_engineer_gemini/sub_agents/documentation/__init__.py b/contributing/samples/software_engineer_gemini/sub_agents/documentation/__init__.py new file mode 100644 index 0000000000..bd20af3d52 --- /dev/null +++ b/contributing/samples/software_engineer_gemini/sub_agents/documentation/__init__.py @@ -0,0 +1 @@ +"""Documentation agent for generating code and project documentation.""" diff --git a/contributing/samples/software_engineer_gemini/sub_agents/documentation/agent.py b/contributing/samples/software_engineer_gemini/sub_agents/documentation/agent.py new file mode 100644 index 0000000000..18cc3b046f --- /dev/null +++ b/contributing/samples/software_engineer_gemini/sub_agents/documentation/agent.py @@ -0,0 +1,28 @@ +"""Documentation Agent Implementation.""" + +from google.adk.agents import LlmAgent + +# Import codebase search tool from the tools module +from ...tools import codebase_search_tool +from ...tools.filesystem import edit_file_tool, list_dir_tool, read_file_tool +from ...tools.search import google_search_grounding +from ...tools.shell_command import execute_vetted_shell_command_tool +from . import prompt + +documentation_agent = LlmAgent( + model="gemini-1.5-pro-001", + name="documentation_agent", + description="Agent specialized in writing and updating documentation", + instruction=prompt.DOCUMENTATION_AGENT_INSTR, + tools=[ + read_file_tool, + list_dir_tool, + edit_file_tool, + codebase_search_tool, + execute_vetted_shell_command_tool, + google_search_grounding, + ], + output_key="documentation", +) + +# Placeholder for actual tool implementation diff --git a/contributing/samples/software_engineer_gemini/sub_agents/documentation/prompt.py b/contributing/samples/software_engineer_gemini/sub_agents/documentation/prompt.py new file mode 100644 index 0000000000..dd89e80cb6 --- /dev/null +++ b/contributing/samples/software_engineer_gemini/sub_agents/documentation/prompt.py @@ -0,0 +1,67 @@ +# ruff: noqa +"""Prompt for the documentation agent.""" + +DOCUMENTATION_AGENT_INSTR = """ +You are an expert Documentation agent. Your task is to generate clear, accurate, and comprehensive documentation for code, APIs, and projects, adhering to best practices. + +## Core Documentation Workflow: + +1. **Identify Scope & Audience:** Determine what needs documenting (e.g., a function, class, module, API endpoint, the whole project) and for whom (e.g., end-users, other developers). + +2. **Analyze Code & Context:** + * Use `read_file_content` to thoroughly understand the code to be documented. + * Use `list_directory_contents` to grasp the project structure and relationships. + * Use `codebase_search` to find how the code is used, its dependencies, and its purpose within the larger system. + +3. **Research Standards & Examples (If Needed):** + * Use `google_search_grounding` to look up relevant documentation standards (e.g., Javadoc, Google Style Python Docstrings, OpenAPI), formatting conventions (e.g., Markdown, reStructuredText), or examples of good documentation. + +4. **Generate Documentation Content:** + * Write clear, concise, and accurate explanations. + * Include essential information like purpose, parameters, return values, usage examples, error conditions, and required setup. + * Tailor the language and detail level to the intended audience. + * For code documentation, generate well-formatted docstrings or comments. + * For project/API documentation, structure the content logically (e.g., in a README.md, API reference pages). + +5. **Run Doc Generators (Optional):** + * If the project uses documentation generation tools (e.g., Sphinx, Javadoc, Doxygen), identify the relevant command (check config files like `conf.py`, `pom.xml`, `Makefile`). + * Use the safe shell command workflow (see reference below) to run the generator tool and build the documentation. + +6. **Write/Update Documentation Files:** + * **Output Format:** Prepare the final documentation content. This might be docstrings/comments to insert into code, or full file content (e.g., for a README.md). + * Use `edit_file_content` to: + * Create or update documentation files (like README.md, .rst files). + * Insert generated docstrings/comments into the corresponding source code files. + * Remember `edit_file_content` respects session approval settings; inform the user if approval is needed. + +## Context: + +Current project context: + +{project_context} + + +## Shell Command Execution Workflow Reference: +(Use this workflow when executing documentation generator commands in Step 5) +- **Tools:** `configure_shell_approval`, `configure_shell_whitelist`, `check_command_exists_tool`, `check_shell_command_safety`, `execute_vetted_shell_command`. +- **Workflow:** Follow the standard 5 steps: Check Existence, Check Safety, Handle Approval, Execute, Handle Errors. + +## Task: Generate or Update Documentation + +### Execution Strategy: + +1. **Tool Usage:** Leverage available documentation generation tools (e.g., `jsdoc`, `sphinx`, `godoc`, etc.) if appropriate and available. Check using `check_command_exists_tool`. +2. **File IO:** Use `read_file` and `edit_file` to interact with documentation files. +3. **Shell Commands:** If using external tools, follow the strict shell command execution rules: + * Check existence with `check_command_exists_tool`. + * Check safety with `check_shell_command_safety`. + * Execute ONLY safe commands with `execute_vetted_shell_command`. +4. **Code Generation:** If generating documentation *within* code files (e.g., docstrings), use `edit_file` carefully. +5. **Output:** Respond with a summary of actions taken and the paths to the modified or created documentation files. + +### Tools: + +- **File I/O:** `read_file`, `edit_file` +- **Shell:** `configure_shell_approval`, `configure_shell_whitelist`, `check_command_exists_tool`, `check_shell_command_safety`, `execute_vetted_shell_command`. +- **Knowledge:** `codebase_search`, `file_search` +""" diff --git a/contributing/samples/software_engineer_gemini/sub_agents/testing/__init__.py b/contributing/samples/software_engineer_gemini/sub_agents/testing/__init__.py new file mode 100644 index 0000000000..ecf12adc05 --- /dev/null +++ b/contributing/samples/software_engineer_gemini/sub_agents/testing/__init__.py @@ -0,0 +1 @@ +"""Testing agent for generating test cases and testing strategies.""" diff --git a/contributing/samples/software_engineer_gemini/sub_agents/testing/agent.py b/contributing/samples/software_engineer_gemini/sub_agents/testing/agent.py new file mode 100644 index 0000000000..e105b69ce6 --- /dev/null +++ b/contributing/samples/software_engineer_gemini/sub_agents/testing/agent.py @@ -0,0 +1,28 @@ +"""Testing Agent Implementation.""" + +from google.adk.agents import LlmAgent + +# Import codebase search tool from the tools module +from ...tools import codebase_search_tool +from ...tools.filesystem import edit_file_tool, list_dir_tool, read_file_tool +from ...tools.search import google_search_grounding +from ...tools.shell_command import execute_vetted_shell_command_tool +from . import prompt + +testing_agent = LlmAgent( + model="gemini-1.5-pro-001", + name="testing_agent", + description="Agent specialized in writing and running tests", + instruction=prompt.TESTING_AGENT_INSTR, + tools=[ + read_file_tool, + list_dir_tool, + edit_file_tool, + codebase_search_tool, + execute_vetted_shell_command_tool, + google_search_grounding, + ], + output_key="testing", +) + +# Placeholder for actual tool implementation diff --git a/contributing/samples/software_engineer_gemini/sub_agents/testing/prompt.py b/contributing/samples/software_engineer_gemini/sub_agents/testing/prompt.py new file mode 100644 index 0000000000..00fc8956a5 --- /dev/null +++ b/contributing/samples/software_engineer_gemini/sub_agents/testing/prompt.py @@ -0,0 +1,72 @@ +# ruff: noqa +"""Prompt for the testing agent.""" + +TESTING_AGENT_INSTR = """ +You are a diligent Testing agent. Your mission is to help developers create comprehensive and effective automated tests for their code, ensuring reliability and maintainability. + +You generate test cases (unit, integration), explain testing strategies, suggest improvements to test suites, and aim to improve test coverage. + +## Core Responsibilities: + +1. **Tool Discovery (Preliminary Step):** Before writing tests, identify the project's testing framework and execution command. + * **Check Project Configuration:** Examine configuration files (`pyproject.toml`, `package.json`, `pom.xml`, `build.gradle`, `Makefile`, etc.) for test scripts, dependencies, or specific test runner configurations. + * **Language-Specific Hints:** Based on the project language, look for common test runners and commands: + * Python: `pytest`, `unittest` (often run via `python -m unittest`). + * JavaScript/TypeScript: `jest`, `mocha`, `vitest` (usually run via `npm test`, `yarn test`, or specific package scripts). + * Java: `JUnit`, `TestNG` (typically run via `mvn test` or `gradle test`). + * Go: Standard `go test ./...` command. + * (Adapt based on detected language). + * **Verify Availability:** Use `check_command_exists_tool` to verify that the likely test execution command (e.g., `pytest`, `npm`, `go`, `mvn`) is available in the environment. Also check for coverage tools if relevant (e.g., `coverage` for Python). + * Report the discovered test command and any identified coverage tools. + +2. **Understand the Code:** + * Use `read_file_content` to fetch the source code of the module/function/class you need to test. + * Use `list_directory_contents` to understand the project structure and determine the correct location for new test files. + * Use `codebase_search` to understand the functionality, dependencies, and usage patterns of the code being tested. + +3. **Generate Tests:** + * Write clear, readable, and maintainable tests. + * Focus on testing public interfaces/APIs. + * Include tests for: + * Happy paths (expected behavior). + * Edge cases and boundary conditions. + * Error handling and invalid inputs. + * Employ mocking, stubbing, or test doubles where necessary to isolate units under test. + * Follow testing best practices for the identified language and framework. + * **Output:** Prepare the complete content for the new or modified test file(s). This content will be used with the `edit_file_content` tool. + +4. **Write Test Files:** + * Use the `edit_file_content` tool to create new test files or add tests to existing ones in the appropriate test directory. + * **Note:** The `edit_file_content` tool respects the session's approval settings (configured via `configure_edit_approval`). If approval is required, you must inform the user and await confirmation before the tool writes the file. + +5. **Run Tests & Coverage (Optional but Recommended):** + * Execute the discovered test command using the standard safe shell command workflow (see reference below). + * If a coverage tool was identified and is available, run it (also using the safe shell workflow) to report on test coverage for the modified/new code. + * Analyze the results from the test runner and coverage tool. If tests fail, attempt to debug based on the output. + +## Context: + +Current project context: + +{project_context} + + +## Task: Run Tests and Check Coverage + +### Execution Strategy: + +1. **Identify Test Framework & Command:** + * Analyze project structure, configuration files (`Makefile`, `package.json`, `pom.xml`, `pyproject.toml`, etc.), and code files to determine the testing framework (e.g., `pytest`, `jest`, `JUnit`, `go test`) and the likely command to run tests (potentially including coverage). + * **Verify Availability:** Use `check_command_exists_tool` to verify that the likely test execution command (e.g., `pytest`, `npm`, `go`, `mvn`) is available in the environment. Also check for coverage tools if relevant (e.g., `coverage` for Python). + +2. **Shell Command Execution:** + * Follow the standard shell execution rules rigorously: check existence (`check_command_exists_tool`), check safety (`check_shell_command_safety`), handle approval, execute (`execute_vetted_shell_command`). + * Run the identified test command(s). + * Capture stdout/stderr. + +### Shell Command Execution Workflow Reference: +(Use this workflow when executing test/coverage commands in Step 2) + +- **Tools:** `configure_shell_approval`, `configure_shell_whitelist`, `check_command_exists_tool` (used in Step 1), `check_shell_command_safety`, `execute_vetted_shell_command`. +- **Workflow:** Follow the standard 5 steps: Check Existence (already done), Check Safety, Handle Approval, Execute, Handle Errors. +""" diff --git a/contributing/samples/software_engineer_gemini/tools/__init__.py b/contributing/samples/software_engineer_gemini/tools/__init__.py new file mode 100644 index 0000000000..e44d43dcf4 --- /dev/null +++ b/contributing/samples/software_engineer_gemini/tools/__init__.py @@ -0,0 +1,78 @@ +# ruff: noqa: I001, F401 +"""Tools for the Software Engineer Multi-Agent.""" + +from . import ( + analysis_state, + code_analysis, + code_search, + filesystem, + search, + shell_command, + system_info, +) + +# Export code analysis tools +from .code_analysis import ( + analyze_code_tool, + get_analysis_issues_by_severity_tool, + suggest_code_fixes_tool, +) + +# Export the code search tool for easier imports +from .code_search import codebase_search_tool + +# Export filesystem tools +from .filesystem import ( + read_file_tool, + list_dir_tool, + edit_file_tool, + configure_approval_tool, +) + +# Export shell command tools +from .shell_command import ( + check_command_exists_tool, + check_shell_command_safety_tool, + configure_shell_approval_tool, + configure_shell_whitelist_tool, + execute_vetted_shell_command_tool, +) + +# Export search tools +from .search import google_search_grounding + +from .system_info import get_os_info_tool + +# Import system info tools + +# Import the placeholder memory persistence tools +from .persistent_memory_tool import ( + save_current_session_to_file_tool, + load_memory_from_file_tool, +) + +__all__ = [ + # Filesystem Tools + "read_file_tool", + "list_dir_tool", + "edit_file_tool", + "configure_approval_tool", + # Shell Command Tools + "check_command_exists_tool", + "check_shell_command_safety_tool", + "configure_shell_approval_tool", + "configure_shell_whitelist_tool", + "execute_vetted_shell_command_tool", + # Code Analysis Tools (add if needed by root agent, or keep in sub-agent) + # "analyze_code_tool", + # "get_analysis_issues_by_severity_tool", + # "suggest_code_fixes_tool", + # Search Tools + "google_search_grounding", + "codebase_search_tool", + # System Info Tools + "get_os_info_tool", + # Placeholder Persistent Memory Tools + "save_current_session_to_file_tool", + "load_memory_from_file_tool", +] diff --git a/contributing/samples/software_engineer_gemini/tools/analysis_state.py b/contributing/samples/software_engineer_gemini/tools/analysis_state.py new file mode 100644 index 0000000000..e1e99e264c --- /dev/null +++ b/contributing/samples/software_engineer_gemini/tools/analysis_state.py @@ -0,0 +1,37 @@ +# software_engineer/tools/analysis_state.py +"""Tool to access analysis results stored in session state.""" + +# TODO: This tool reads from tool_context.state['analysis_issues']. +# It requires another tool or agent (e.g., an enhanced analyze_code_tool or +# the code_review agent after running linters) to populate this state key +# with structured issue data (e.g., list of dicts) for this tool to be useful. + +from typing import Any, Dict + +from google.adk.tools import FunctionTool, ToolContext + + +def get_analysis_issues(tool_context: ToolContext) -> Dict[str, Any]: + """ + Retrieves code analysis issues stored under the key 'analysis_issues' in the session state. + + Args: + tool_context: The tool context from ADK, providing access to session state. + + Returns: + A dictionary containing the list of analysis issues under the key 'issues', + or an empty list if the 'analysis_issues' key is not found or not a list. + """ + # Get the issues list, default to empty list if key not found or value is None + issues = tool_context.state.get("analysis_issues") + # Ensure it's a list, return empty list otherwise + if not isinstance(issues, list): + issues = [] + return {"issues": issues} + + +# Define the tool using FunctionTool +get_analysis_issues_tool = FunctionTool( + func=get_analysis_issues, + # Description comes from func docstring +) diff --git a/contributing/samples/software_engineer_gemini/tools/code_analysis.py b/contributing/samples/software_engineer_gemini/tools/code_analysis.py new file mode 100644 index 0000000000..6a66621ffd --- /dev/null +++ b/contributing/samples/software_engineer_gemini/tools/code_analysis.py @@ -0,0 +1,419 @@ +"""Code analysis tool for the software engineer agent. + +This tool performs static analysis on code files using language-specific analyzers +and provides detailed reports on quality, complexity, and potential issues. +""" + +import os +import re +from enum import Enum +from typing import Any, Dict, List, Optional + +from google.adk.tools import FunctionTool, ToolContext +from pydantic import BaseModel, Field + +# Third-party analysis libraries - using try/except to make dependencies optional +try: + import pylint.lint + import pylint.reporters.text + + PYLINT_AVAILABLE = True +except ImportError: + PYLINT_AVAILABLE = False + +try: + # NOTE: flake8.api.legacy is no longer available in newer versions of flake8 + # We'll use a different approach for flake8 + import flake8 # noqa: F401 + from flake8.main.application import Application + + FLAKE8_AVAILABLE = True +except ImportError: + FLAKE8_AVAILABLE = False + +try: + import radon.complexity + import radon.metrics + + RADON_AVAILABLE = True +except ImportError: + RADON_AVAILABLE = False + +try: + import bandit + from bandit.core import manager as bandit_manager + + BANDIT_AVAILABLE = True +except ImportError: + BANDIT_AVAILABLE = False + + +class AnalysisSeverity(str, Enum): + """Severity levels for analysis issues.""" + + CRITICAL = "critical" + ERROR = "error" + WARNING = "warning" + INFO = "info" + + +class CodeIssue(BaseModel): + """Model for code issues identified during analysis.""" + + line: Optional[int] = None + column: Optional[int] = None + severity: AnalysisSeverity = AnalysisSeverity.INFO + message: str + code: Optional[str] = None + source: str # The tool that found this issue (e.g., "pylint", "flake8") + + +class AnalyzeCodeInput(BaseModel): + """Input model for code analysis.""" + + file_path: str = Field(description="Path to the file to analyze") + + +class CodeAnalysisResult(BaseModel): + """Result model for code analysis.""" + + file_path: str + language: str + lines_of_code: int + issues: List[CodeIssue] = [] + metrics: Dict[str, Any] = {} + status: str + error: Optional[str] = None + + +def detect_language(file_path: str) -> str: + """ + Detect the programming language from a file extension. + + Args: + file_path: Path to the file + + Returns: + String representing the language + """ + ext = os.path.splitext(file_path)[1].lower() + language_map = { + ".py": "python", + ".js": "javascript", + ".ts": "typescript", + ".java": "java", + ".c": "c", + ".cpp": "cpp", + ".h": "c_header", + ".hpp": "cpp_header", + ".go": "go", + ".rb": "ruby", + ".php": "php", + ".cs": "csharp", + ".rs": "rust", + ".swift": "swift", + ".kt": "kotlin", + ".sh": "shell", + } + return language_map.get(ext, "unknown") + + +def analyze_python_code(file_path: str, code: str) -> Dict[str, Any]: + """ + Analyze Python code using pylint, flake8, radon, and bandit. + + Args: + file_path: Path to the Python file + code: Content of the file + + Returns: + Dict with analysis results + """ + issues = [] + metrics = {} + + # Run pylint + if PYLINT_AVAILABLE: + try: + from io import StringIO + + output = StringIO() + reporter = pylint.reporters.text.TextReporter(output) + + pylint.lint.Run([file_path, "--output-format=text"], reporter=reporter, exit=False) + + pylint_output = output.getvalue() + # TODO: Sonar Report - https://sonarcloud.io/project/security_hotspots?id=BlueCentre_code-agent&pullRequest=19&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true + # NOTE: Make sure the regex used here, which is vulnerable to polynomial runtime due to backtracking, cannot lead to denial of service. + # Parse pylint output + pattern = r"([A-Z]):\s*(\d+),\s*(\d+):\s*(.+)\s*\(([A-Z0-9]+)\)" + for match in re.finditer(pattern, pylint_output): + severity_code, line, col, message, code = match.groups() + severity_map = { + "E": AnalysisSeverity.ERROR, + "F": AnalysisSeverity.CRITICAL, + "W": AnalysisSeverity.WARNING, + "C": AnalysisSeverity.INFO, + "R": AnalysisSeverity.INFO, + } + + issues.append( + CodeIssue( + line=int(line), + column=int(col), + severity=severity_map.get(severity_code, AnalysisSeverity.INFO), + message=message.strip(), + code=code, + source="pylint", + ) + ) + except Exception as e: + issues.append(CodeIssue(severity=AnalysisSeverity.ERROR, message=f"Error running pylint: {e!s}", source="analyzer")) + + # Run flake8 + if FLAKE8_AVAILABLE: + try: + # Use the Application class directly instead of the legacy API + flake8_app = Application() + flake8_app.initialize([file_path]) + flake8_app.run_checks([file_path]) + flake8_app.formatter.start() + + for file_errors in flake8_app.guide.stats.statistics_for(""): + for error in file_errors: + if len(error) >= 4: # Make sure the error has all components + line_num, col_num, message = error[0], error[1], error[2] + # line_num, col_num, message, code_obj = error[0], error[1], error[2], error[3] + + severity = AnalysisSeverity.WARNING + if message.startswith("E"): + severity = AnalysisSeverity.ERROR + elif message.startswith("F"): + severity = AnalysisSeverity.CRITICAL + + issues.append(CodeIssue(line=line_num, column=col_num, severity=severity, message=message, code=message.split(" ")[0], source="flake8")) + + flake8_app.formatter.stop() + flake8_app.report_errors() + except Exception as e: + issues.append(CodeIssue(severity=AnalysisSeverity.ERROR, message=f"Error running flake8: {e!s}", source="analyzer")) + + # Run radon for complexity metrics + if RADON_AVAILABLE: + try: + # Cyclomatic Complexity + cc_blocks = radon.complexity.cc_visit(code) + avg_complexity = sum(block.complexity for block in cc_blocks) / len(cc_blocks) if cc_blocks else 0 + + # Maintainability Index + mi_score = radon.metrics.mi_visit(code, multi=True) + + metrics["cyclomatic_complexity"] = { + "average": avg_complexity, + "blocks": [{"name": block.name, "complexity": block.complexity, "rank": block.rank, "line": block.lineno} for block in cc_blocks], + } + + metrics["maintainability_index"] = mi_score + + # Flag high complexity functions + for block in cc_blocks: + if block.complexity > 10: + severity = AnalysisSeverity.WARNING + if block.complexity > 20: + severity = AnalysisSeverity.ERROR + if block.complexity > 30: + severity = AnalysisSeverity.CRITICAL + + issues.append( + CodeIssue( + line=block.lineno, + severity=severity, + message=f"High cyclomatic complexity ({block.complexity}) in {block.name}", + code="R001", + source="radon", + ) + ) + except Exception as e: + issues.append(CodeIssue(severity=AnalysisSeverity.ERROR, message=f"Error calculating code complexity: {e!s}", source="analyzer")) + + # Run bandit for security analysis + if BANDIT_AVAILABLE: + try: + mgr = bandit_manager.BanditManager() + mgr.discover_files([file_path]) + mgr.run_tests() + + for issue in mgr.get_issue_list(): + severity_map = { + bandit.constants.HIGH: AnalysisSeverity.CRITICAL, + bandit.constants.MEDIUM: AnalysisSeverity.ERROR, + bandit.constants.LOW: AnalysisSeverity.WARNING, + } + + issues.append( + CodeIssue( + line=issue.lineno, + severity=severity_map.get(issue.severity, AnalysisSeverity.INFO), + message=issue.text, + code=issue.test_id, + source="bandit", + ) + ) + except Exception as e: + issues.append(CodeIssue(severity=AnalysisSeverity.ERROR, message=f"Error running security analysis: {e!s}", source="analyzer")) + + return {"issues": issues, "metrics": metrics} + + +def analyze_javascript_code(file_path: str, code: str) -> Dict[str, Any]: + """ + Analyze JavaScript code. + + Currently a placeholder for JavaScript analysis. + + Args: + file_path: Path to the JavaScript file + code: Content of the file + + Returns: + Dict with analysis results + """ + # Placeholder for JavaScript analysis + # In a real implementation, we would integrate tools like ESLint + return {"issues": [CodeIssue(severity=AnalysisSeverity.INFO, message="JavaScript analysis not yet implemented", source="analyzer")], "metrics": {}} + + +def _analyze_code(file_path: str, tool_context: ToolContext) -> Dict[str, Any]: + """ + Analyze code in a file for quality issues. + + Args: + file_path: Path to the file to analyze. + tool_context: The tool context from ADK. + + Returns: + Dict containing analysis results. + """ + try: + if not os.path.exists(file_path): + return {"error": f"File {file_path} does not exist", "status": "Failed"} + + with open(file_path, "r", encoding="utf-8") as file: + code = file.read() + + # Store the code in the state for the agent to access + tool_context.state["analyzed_code"] = code + tool_context.state["analyzed_file"] = file_path + + # Detect language + language = detect_language(file_path) + + # Initialize result object + result = CodeAnalysisResult(file_path=file_path, language=language, lines_of_code=len(code.split("\n")), status="Analysis complete") + + # Analyze based on language + if language == "python": + analysis = analyze_python_code(file_path, code) + result.issues = analysis["issues"] + result.metrics = analysis["metrics"] + elif language in ["javascript", "typescript"]: + analysis = analyze_javascript_code(file_path, code) + result.issues = analysis["issues"] + result.metrics = analysis["metrics"] + else: + result.issues.append(CodeIssue(severity=AnalysisSeverity.INFO, message=f"Analysis for {language} is not yet supported", source="analyzer")) + + # Store analysis issues in state for other tools to use + tool_context.state["analysis_issues"] = [issue.dict() for issue in result.issues] + + # Generate summary statistics + issue_counts = { + "critical": sum(1 for i in result.issues if i.severity == AnalysisSeverity.CRITICAL), + "error": sum(1 for i in result.issues if i.severity == AnalysisSeverity.ERROR), + "warning": sum(1 for i in result.issues if i.severity == AnalysisSeverity.WARNING), + "info": sum(1 for i in result.issues if i.severity == AnalysisSeverity.INFO), + } + result.metrics["issue_summary"] = issue_counts + + return result.dict() + except Exception as e: + return {"error": f"Error analyzing file: {e!s}", "status": "Failed"} + + +# Define the tool using FunctionTool +analyze_code_tool = FunctionTool(func=_analyze_code) + + +def get_issues_by_severity(tool_context: ToolContext, severity: Optional[str] = None) -> Dict[str, Any]: + """ + Retrieves code analysis issues filtered by severity. + + Args: + tool_context: The tool context from ADK. + severity: Optional severity level to filter by (critical, error, warning, info) + + Returns: + Dict containing filtered issues list. + """ + # Get all issues + issues = tool_context.state.get("analysis_issues", []) + + # If no severity specified, return all issues + if not severity: + return {"issues": issues} + + # Filter by severity + filtered_issues = [issue for issue in issues if issue.get("severity", "").lower() == severity.lower()] + + return {"issues": filtered_issues, "count": len(filtered_issues), "total_issues": len(issues), "severity": severity} + + +# Define additional tools to work with analysis results +get_analysis_issues_by_severity_tool = FunctionTool(func=get_issues_by_severity) + + +def suggest_fixes(tool_context: ToolContext) -> Dict[str, Any]: + """ + Analyzes issues and suggests fixes based on common patterns. + + Args: + tool_context: The tool context from ADK. + + Returns: + Dict containing suggested fixes for detected issues. + """ + issues = tool_context.state.get("analysis_issues", []) + + suggested_fixes = [] + + # Common patterns and their fixes + for issue in issues: + code = issue.get("code", "") + message = issue.get("message", "") + suggestion = None + + # Unused import suggestions + if "unused import" in message.lower(): + suggestion = f"Remove the unused import at line {issue.get('line')}" + + # Undefined variable suggestions + elif "undefined variable" in message.lower(): + var_name = message.split("'")[1] if "'" in message else "" + suggestion = f"Define variable '{var_name}' before use or check for typo" + + # Line too long + elif code == "E501" or "line too long" in message.lower(): + suggestion = f"Break the long line at {issue.get('line')} into multiple lines" + + # Complexity suggestions + elif "complexity" in message.lower(): + suggestion = f"Refactor the complex function at line {issue.get('line')} into smaller functions" + + if suggestion: + suggested_fixes.append({"issue": issue, "suggestion": suggestion}) + + return {"suggested_fixes": suggested_fixes, "count": len(suggested_fixes), "analyzed_file": tool_context.state.get("analyzed_file")} + + +# Define a tool for suggesting fixes +suggest_code_fixes_tool = FunctionTool(func=suggest_fixes) diff --git a/contributing/samples/software_engineer_gemini/tools/code_search.py b/contributing/samples/software_engineer_gemini/tools/code_search.py new file mode 100644 index 0000000000..36b1db1b71 --- /dev/null +++ b/contributing/samples/software_engineer_gemini/tools/code_search.py @@ -0,0 +1,78 @@ +"""Code search functionality for software engineer agents using ripgrep.""" + +import json +import subprocess +from typing import Any, Dict, List, Optional + +from google.adk.tools import FunctionTool + + +def ripgrep_code_search(query: str, target_directories: Optional[List[str]] = None, explanation: Optional[str] = None) -> Dict[str, Any]: + """ + Perform a code search using ripgrep (rg) and return the results. + + Args: + query: The search query to find relevant code + target_directories: Optional list of directories to search in (glob patterns supported) + explanation: Optional explanation of why this search is being performed + + Returns: + Dictionary containing search results with snippets and file information + """ + try: + # Default to search in current directory if none specified + search_paths = target_directories or ["."] + + results = [] + for path in search_paths: + # Build the ripgrep command + # Using --json for structured output + # --context for showing surrounding lines + cmd = [ + "rg", + "--json", + "--context", + "2", # Show 2 lines before and after matches + "--max-columns", + "1000", # Reasonable line length limit + query, + ] + + # Add path to search + cmd.append(path) + + # Execute the search + process = subprocess.run( + cmd, + capture_output=True, + text=True, + check=False, # Don't raise exception if nothing found + ) + + # Process the output - each line is a JSON object + for line in process.stdout.strip().split("\n"): + if not line: + continue + + try: + data = json.loads(line) + + # Only process match data + if data.get("type") == "match": + file_path = data.get("data", {}).get("path", {}).get("text", "") + line_number = data.get("data", {}).get("line_number", 0) + match_content = data.get("data", {}).get("lines", {}).get("text", "").strip() + + results.append({"file": file_path, "line": line_number, "content": match_content}) + except json.JSONDecodeError: + # Skip lines that aren't valid JSON + continue + + return {"snippets": results, "status": "success", "query": query, "explanation": explanation or "Code search results"} + + except Exception as e: + return {"snippets": [], "status": "error", "error_message": str(e), "query": query} + + +# Create FunctionTool wrapper for ripgrep code search +codebase_search_tool = FunctionTool(func=ripgrep_code_search) diff --git a/contributing/samples/software_engineer_gemini/tools/filesystem.py b/contributing/samples/software_engineer_gemini/tools/filesystem.py new file mode 100644 index 0000000000..44624a17eb --- /dev/null +++ b/contributing/samples/software_engineer_gemini/tools/filesystem.py @@ -0,0 +1,193 @@ +# code_agent/agent/software_engineer/software_engineer/tools/filesystem_tools.py +import logging +import os +from typing import Any, Dict + +from google.adk.tools import FunctionTool, ToolContext + +logger = logging.getLogger(__name__) + +# Consider adding a WORKSPACE_ROOT validation here for security +# WORKSPACE_ROOT = os.path.abspath(".") # Example: Use current working directory + + +def read_file_content(filepath: str) -> Dict[str, Any]: + """ + Reads the content of a file from the local filesystem. + + Args: + filepath: The relative or absolute path to the file. + Relative paths are resolved from the agent's current working directory. + (Security Note: Path validation should be implemented to restrict access). + + Returns: + A dictionary with: + - {'status': 'success', 'content': 'file_content_string'} on success. + - {'status': 'error', 'error_type': str, 'message': str} on failure. + Possible error_types: 'FileNotFound', 'PermissionDenied', 'IOError', 'SecurityViolation' (if implemented). + """ + logger.info(f"Attempting to read file: {filepath}") + # Add path validation/sandboxing here before opening + # Example: + # abs_path = os.path.abspath(filepath) + # if not abs_path.startswith(WORKSPACE_ROOT): + # message = f"Access denied: Path '{filepath}' is outside the allowed workspace." + # logger.error(message) + # return {"status": "error", "error_type": "SecurityViolation", "message": message} + try: + with open(filepath, "r", encoding="utf-8") as f: + content = f.read() + logger.info(f"Successfully read file: {filepath}") + return {"status": "success", "content": content} + except FileNotFoundError: + message = f"File not found at path '{filepath}'." + logger.error(message) + return {"status": "error", "error_type": "FileNotFound", "message": message} + except PermissionError: + message = f"Permission denied when trying to read file '{filepath}'." + logger.error(message) + return {"status": "error", "error_type": "PermissionDenied", "message": message} + except Exception as e: + message = f"An unexpected error occurred while reading file '{filepath}': {e}" + logger.error(message, exc_info=True) + return {"status": "error", "error_type": "IOError", "message": message} + + +def list_directory_contents(directory_path: str) -> Dict[str, Any]: + """ + Lists the contents (files and directories) of a directory on the local filesystem. + + Args: + directory_path: The relative or absolute path to the directory. + Relative paths are resolved from the agent's current working directory. + (Security Note: Path validation should be implemented to restrict access). + + Returns: + A dictionary with: + - {'status': 'success', 'contents': ['item1', 'item2', ...]} on success. + - {'status': 'error', 'error_type': str, 'message': str} on failure. + Possible error_types: 'NotADirectory', 'FileNotFound', 'PermissionDenied', 'IOError', 'SecurityViolation' (if implemented). + """ + logger.info(f"Attempting to list directory: {directory_path}") + # Add path validation/sandboxing here + # Example: + # abs_path = os.path.abspath(directory_path) + # if not abs_path.startswith(WORKSPACE_ROOT): + # message = f"Access denied: Path '{directory_path}' is outside the allowed workspace." + # logger.error(message) + # return {"status": "error", "error_type": "SecurityViolation", "message": message} + try: + if not os.path.isdir(directory_path): + message = f"The specified path '{directory_path}' is not a valid directory." + logger.warning(message) + return {"status": "error", "error_type": "NotADirectory", "message": message} + contents = os.listdir(directory_path) + logger.info(f"Successfully listed directory: {directory_path}") + return {"status": "success", "contents": contents} + except FileNotFoundError: + message = f"Directory not found at path '{directory_path}'." + logger.error(message) + return {"status": "error", "error_type": "FileNotFound", "message": message} + except PermissionError: + message = f"Permission denied when trying to list directory '{directory_path}'." + logger.error(message) + return {"status": "error", "error_type": "PermissionDenied", "message": message} + except Exception as e: + message = f"An unexpected error occurred while listing directory '{directory_path}': {e}" + logger.error(message, exc_info=True) + return {"status": "error", "error_type": "IOError", "message": message} + + +def edit_file_content(filepath: str, content: str, tool_context: ToolContext) -> Dict[str, Any]: + """ + Writes content to a file or proposes the write, requiring user approval based on session state. + Creates the file if it does not exist (including parent directories). + Overwrites the file if it already exists (only if approval is not required or already granted). + + Checks the 'require_edit_approval' flag in session state (defaults to True). + If True, returns a 'pending_approval' status without writing. + If False, writes the file and returns 'success' or 'error'. + + Args: + filepath: The relative or absolute path to the file. + Relative paths are resolved from the agent's current working directory. + (Security Note: Path validation should be implemented to restrict access). + content: The new content to write to the file. + + Returns: + A dictionary with: + - {'status': 'pending_approval', 'proposed_filepath': str, 'proposed_content': str, 'message': str} if approval is required. + - {'status': 'success', 'message': 'Success message'} on successful write (when approval not required). + - {'status': 'error', 'error_type': str, 'message': str} on failure during write or validation. + Possible error_types: 'PermissionDenied', 'IOError', 'SecurityViolation' (if implemented). + """ + logger.info(f"Checking approval requirement for writing to file: {filepath}") + + # Add path validation/sandboxing here FIRST + # Example: + # abs_path = os.path.abspath(filepath) + # if not abs_path.startswith(WORKSPACE_ROOT): + # message = f"Access denied: Path '{filepath}' is outside the allowed workspace." + # logger.error(message) + # return {"status": "error", "error_type": "SecurityViolation", "message": message} + + needs_approval = tool_context.state.get("require_edit_approval", True) + + if needs_approval: + logger.info(f"Approval required for file edit: {filepath}. Returning pending status.") + return { + "status": "pending_approval", + "proposed_filepath": filepath, + "proposed_content": content, + "message": f"Approval required to write to '{filepath}'. User confirmation needed.", + } + + # Proceed with write only if approval is not required + logger.info(f"Approval not required. Proceeding with write to file: {filepath}") + try: + # Ensure the directory exists + dir_path = os.path.dirname(filepath) + if dir_path: # Ensure dir_path is not empty (happens for root-level files) + os.makedirs(dir_path, exist_ok=True) # Creates parent dirs if needed + + # Consider atomic write here: write to temp file, then os.replace() + with open(filepath, "w", encoding="utf-8") as f: + f.write(content) + message = f"Successfully wrote content to '{filepath}'." + logger.info(message) + return {"status": "success", "message": message} + except PermissionError: + message = f"Permission denied when trying to write to file '{filepath}'." + logger.error(message) + return {"status": "error", "error_type": "PermissionDenied", "message": message} + except Exception as e: + message = f"An unexpected error occurred while writing to file '{filepath}': {e}" + logger.error(message, exc_info=True) + return {"status": "error", "error_type": "IOError", "message": message} + + +def configure_edit_approval(require_approval: bool, tool_context: ToolContext) -> Dict[str, Any]: + """ + Configures whether file edits require user approval for the current session. + Sets the 'require_edit_approval' flag in the session state. + + Args: + require_approval: Set to True to require approval (default), False to allow direct edits. + + Returns: + A dictionary confirming the setting change: + - {'status': 'success', 'message': 'Confirmation message'} + """ + logger.info(f"Setting 'require_edit_approval' state to: {require_approval}") + tool_context.state["require_edit_approval"] = require_approval + message = f"File edit approval requirement set to: {require_approval} for this session." + logger.info(message) + return {"status": "success", "message": message} + + +# Wrap functions with FunctionTool +# Note: The return type for the tool schema remains the base function's return type hint (Dict[str, Any]) +read_file_tool = FunctionTool(read_file_content) +list_dir_tool = FunctionTool(list_directory_contents) +edit_file_tool = FunctionTool(edit_file_content) +configure_approval_tool = FunctionTool(configure_edit_approval) diff --git a/contributing/samples/software_engineer_gemini/tools/memory_tools.py b/contributing/samples/software_engineer_gemini/tools/memory_tools.py new file mode 100644 index 0000000000..5193dcc198 --- /dev/null +++ b/contributing/samples/software_engineer_gemini/tools/memory_tools.py @@ -0,0 +1,113 @@ +"""Tools for interacting with the persistent memory service.""" + +import logging +from typing import Any, Dict, List + +from google.adk.tools import FunctionTool +from google.adk.tools.tool_context import ToolContext + +logger = logging.getLogger(__name__) + + +def add_memory_fact_tool(tool_context: ToolContext, entity_name: str, fact_content: str) -> str: + """Adds a discrete fact to the memory service for the current session.""" + # Access services via _invocation_context + invocation_context = getattr(tool_context, "_invocation_context", None) + if not invocation_context: + logger.error("Invocation context not found in tool context for add_memory_fact.") + return "Error: Invocation context unavailable." + + memory_service = getattr(invocation_context, "memory_service", None) + # Session ID might be available directly on session object within invocation_context? + # Let's check for session first, then session_id directly + session = getattr(invocation_context, "session", None) + session_id = getattr(session, "id", None) if session else None + if not session_id: + # Fallback: check if session_id is directly on invocation_context (less likely) + session_id = getattr(invocation_context, "session_id", None) + + # --- Remove Debug --- # + # logger.warning(f"Inspecting tool_context in add_memory_fact_tool:") + # try: + # context_vars = vars(tool_context) + # logger.warning(f"vars(tool_context): {context_vars}") + # except TypeError: + # logger.warning("vars() failed, likely no __dict__. Trying dir():") + # context_dir = dir(tool_context) + # logger.warning(f"dir(tool_context): {context_dir}") + # --- End Debug --- + + # Old access method: + # memory_service = getattr(tool_context, "memory_service", None) + # session_id = getattr(tool_context, "session_id", None) + + if not memory_service: + logger.error("Memory service not available in invocation context for add_memory_fact.") + return "Error: Memory service is not available." + if not session_id: + logger.error("Session ID not available in invocation context for add_memory_fact.") + return "Error: Session ID is not available." + + # Check if add_observations method exists (duck typing) + if not hasattr(memory_service, "add_observations") or not callable(memory_service.add_observations): + logger.error("Memory service does not support 'add_observations'.") + return "Error: Memory service does not support adding observations." + + try: + # Structure the observation as a dictionary + observation = {"entity": entity_name, "content": fact_content} + logger.info(f"Adding memory fact for session {session_id}: {observation}") + # Call the underlying service method + memory_service.add_observations(session_id=session_id, observations=[observation]) + return f"Okay, I have remembered that {entity_name} is {fact_content}." + + except Exception as e: + logger.exception(f"Error calling memory_service.add_observations: {e}") + return f"Error: Failed to add fact to memory - {e}" + + +def search_memory_facts_tool(tool_context: ToolContext, query: str) -> List[Dict[str, Any]]: + """Searches discrete facts in the memory service for the current session.""" + # Access services via _invocation_context + invocation_context = getattr(tool_context, "_invocation_context", None) + if not invocation_context: + logger.error("Invocation context not found in tool context for search_memory_facts.") + return [{"error": "Invocation context unavailable."}] + + memory_service = getattr(invocation_context, "memory_service", None) + session = getattr(invocation_context, "session", None) + session_id = getattr(session, "id", None) if session else None + if not session_id: + session_id = getattr(invocation_context, "session_id", None) + + # Old access method: + # memory_service = getattr(tool_context, "memory_service", None) + # session_id = getattr(tool_context, "session_id", None) + + if not memory_service: + logger.error("Memory service not available in invocation context for search_memory_facts.") + return [{"error": "Memory service is not available."}] + if not session_id: + logger.error("Session ID not available in invocation context for search_memory_facts.") + return [{"error": "Session ID is not available."}] + + # Check if search_nodes method exists (duck typing) + if not hasattr(memory_service, "search_nodes") or not callable(memory_service.search_nodes): + logger.error("Memory service does not support 'search_nodes'.") + return [{"error": "Memory service does not support searching facts (nodes)."}] + + try: + logger.info(f"Searching memory facts for session {session_id} with query: '{query}'") + results = memory_service.search_nodes(session_id=session_id, query=query) + # Return the list of dictionaries directly + # The agent's prompt will need to guide it on interpreting this list + return results + + except Exception as e: + logger.exception(f"Error calling memory_service.search_nodes: {e}") + return [{"error": f"Failed to search facts in memory - {e}"}] + + +# Wrap functions with FunctionTool +add_memory_fact = FunctionTool(add_memory_fact_tool) +search_memory_facts = FunctionTool(search_memory_facts_tool) diff --git a/contributing/samples/software_engineer_gemini/tools/persistent_memory_tool.py b/contributing/samples/software_engineer_gemini/tools/persistent_memory_tool.py new file mode 100644 index 0000000000..5a8d0490c3 --- /dev/null +++ b/contributing/samples/software_engineer_gemini/tools/persistent_memory_tool.py @@ -0,0 +1,155 @@ +"""Placeholder tools for manually saving/loading session memory to a file.""" + +import logging +from typing import Any, Dict + +from google.adk.tools import FunctionTool, ToolContext + +logger = logging.getLogger(__name__) + +# Default path for the memory file, could be configurable +DEFAULT_MEMORY_FILE = "./.manual_agent_memory.json" + +# === Tool Implementation Functions (Commented Out) === + + +def _save_current_session_to_file_impl(tool_context: ToolContext, filepath: str = DEFAULT_MEMORY_FILE) -> Dict[str, str]: + """ + (Placeholder) Saves the *current* session's state to a specified JSON file. + NOTE: This is a placeholder and not fully implemented. + + Args: + tool_context: The ADK tool context containing session information. + filepath: The path to the JSON file where the session should be saved. + + Returns: + A dictionary indicating the status of the operation. + """ + # TODO: Implement this tool if manual file-based persistence is needed + # for the standard 'adk run' environment. + # Implications: + # - Requires agent to be explicitly prompted to call this tool. + # - Overwrites the file with only the *current* session, or needs logic + # to merge with existing sessions in the file. + # - Doesn't integrate with the ADK's built-in MemoryService. + # - Needs robust error handling (file I/O, JSON serialization). + + logger.warning("Tool 'save_current_session_to_file' is a placeholder and not implemented.") + # --- Begin Commented Implementation Example --- + # if not hasattr(tool_context, 'session') or not tool_context.session: + # msg = "No active session found in tool_context." + # logger.error(msg) + # return {"status": "error", "message": msg} + # + # session: Session = tool_context.session + # logger.info(f"Attempting to save session {session.session_id} to {filepath}...") + # + # # Logic to load existing data, add/update the current session, and save back + # existing_data = {} + # if os.path.exists(filepath): + # try: + # with open(filepath, 'r', encoding='utf-8') as f: + # existing_data = json.load(f) + # logger.debug(f"Loaded {len(existing_data)} sessions from {filepath}") + # except (IOError, json.JSONDecodeError) as e: + # logger.error(f"Error reading existing memory file {filepath}: {e}. Overwriting may occur.") + # + # session_key = f"{session.app_name}_{session.user_id}_{session.session_id}" + # existing_data[session_key] = session.model_dump(mode='json') + # + # try: + # os.makedirs(os.path.dirname(filepath), exist_ok=True) + # with open(filepath, 'w', encoding='utf-8') as f: + # json.dump(existing_data, f, indent=4) + # logger.info(f"Successfully saved session {session.session_id} to {filepath}.") + # return {"status": "success", "message": f"Session saved to {filepath}"} + # except (IOError, TypeError) as e: + # msg = f"Error writing memory file {filepath}: {e}" + # logger.error(msg) + # return {"status": "error", "message": msg} + # --- End Commented Implementation Example --- + return {"status": "skipped", "message": "Tool is not implemented."} + + +def _load_memory_from_file_impl(query: str, filepath: str = DEFAULT_MEMORY_FILE) -> Dict[str, Any]: + """ + (Placeholder) Loads memory from a JSON file and performs a simple query. + NOTE: This is a placeholder and not fully implemented. + + Args: + query: The natural language query to search for in stored messages. + filepath: The path to the JSON file containing stored sessions. + + Returns: + A dictionary containing the search results or an error message. + """ + # TODO: Implement this tool if manual file-based persistence is needed + # for the standard 'adk run' environment. + # Implications: + # - Requires agent to be explicitly prompted to call this tool instead of load_memory. + # - Requires careful design of the query mechanism (e.g., simple substring search). + # - Doesn't integrate with the ADK's built-in MemoryService. + # - Needs robust error handling (file I/O, JSON deserialization, search logic). + + logger.warning("Tool 'load_memory_from_file' is a placeholder and not implemented.") + # --- Begin Commented Implementation Example --- + # if not os.path.exists(filepath): + # msg = f"Memory file not found: {filepath}" + # logger.error(msg) + # return {"status": "error", "message": msg, "results": []} + # + # try: + # with open(filepath, 'r', encoding='utf-8') as f: + # stored_sessions_data: Dict[str, Dict[str, Any]] = json.load(f) + # logger.info(f"Loaded {len(stored_sessions_data)} sessions from {filepath} for query: '{query}'") + # except (IOError, json.JSONDecodeError) as e: + # msg = f"Error reading memory file {filepath}: {e}" + # logger.error(msg) + # return {"status": "error", "message": msg, "results": []} + # + # results: List[Dict[str, Any]] = [] + # query_lower = query.lower() + # + # for session_key, session_data in stored_sessions_data.items(): + # try: + # # Minimal validation - check for history + # history = session_data.get('history', []) + # if not history: + # continue + # + # session_matched = False + # for message in history: + # if isinstance(message, dict) and 'parts' in message: + # message_text = "".join( + # [part.get('text', '') for part in message['parts'] if isinstance(part, dict)] + # ).lower() + # if query_lower in message_text: + # session_matched = True + # break + # + # if session_matched: + # logger.debug(f"Found match in session {session_key}") + # results.append({"session_key": session_key, "session_data": session_data}) + # # Limit results? Maybe return only the first few matches or most relevant? + # + # except Exception as e: + # logger.warning(f"Error processing session {session_key}: {e}. Skipping.") + # + # logger.info(f"Found {len(results)} relevant session(s) for query: '{query}'") + # return {"status": "success", "results": results} + # --- End Commented Implementation Example --- + return {"status": "skipped", "message": "Tool is not implemented.", "results": []} + + +# === Tool Definitions === + +# Wrap the placeholder functions with FunctionTool +save_current_session_to_file_tool = FunctionTool( + func=_save_current_session_to_file_impl, + # Name and description are inferred from the function docstring +) + +load_memory_from_file_tool = FunctionTool( + func=_load_memory_from_file_impl, + # Name and description are inferred from the function docstring +) diff --git a/contributing/samples/software_engineer_gemini/tools/project_context.py b/contributing/samples/software_engineer_gemini/tools/project_context.py new file mode 100644 index 0000000000..2d41fbf8fd --- /dev/null +++ b/contributing/samples/software_engineer_gemini/tools/project_context.py @@ -0,0 +1,54 @@ +"""Project context tool for the software engineer agent.""" + +import json +import os + +from google.adk.agents.callback_context import CallbackContext + +# Define constants +PROJECT_CONTEXT_KEY = "project_context" +USER_PROFILE_KEY = "user_profile" +DEFAULT_CONTEXT_PATH = os.getenv("SOFTWARE_ENGINEER_CONTEXT", "eval/project_context_empty.json") + + +def load_project_context(callback_context: CallbackContext): + """ + Load the project context and user profile from a JSON file. + + Args: + callback_context: The callback context from ADK. + """ + # Initialize empty context + project_context = {} + user_profile = {} + + try: + if os.path.exists(DEFAULT_CONTEXT_PATH): + with open(DEFAULT_CONTEXT_PATH, "r") as file: + data = json.load(file) + project_context = data.get("project_context", {}) + user_profile = data.get("user_profile", {}) + print(f"\nLoaded project context: {project_context}\n") + print(f"\nLoaded user profile: {user_profile}\n") + except Exception as e: + print(f"Error loading project context: {e}") + + # Set the context in the state + callback_context.state[PROJECT_CONTEXT_KEY] = json.dumps(project_context, indent=2) + callback_context.state[USER_PROFILE_KEY] = json.dumps(user_profile, indent=2) + + +def memorize_context(key: str, value: str, context: dict): + """ + Store information in the project context. + + Args: + key: The key to store the value under. + value: The value to store. + context: The context dictionary to update. + + Returns: + A status message. + """ + context[key] = value + return {"status": f'Stored "{key}": "{value}" in project context'} diff --git a/contributing/samples/software_engineer_gemini/tools/search.py b/contributing/samples/software_engineer_gemini/tools/search.py new file mode 100644 index 0000000000..6b3141d149 --- /dev/null +++ b/contributing/samples/software_engineer_gemini/tools/search.py @@ -0,0 +1,34 @@ +# ruff: noqa +# 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. + +"""Wrapper to Google Search Grounding with custom prompt.""" + +from google.adk.agents import Agent +from google.adk.tools.agent_tool import AgentTool +from google.adk.tools.google_search_tool import google_search + +_search_agent = Agent( + model="gemini-2.0-flash", + name="google_search_grounding", + description="An agent providing Google-search grounding capability", + instruction=""" + Answer the user's question directly using google_search grounding tool; Provide a brief but concise response. + Rather than a detail response, provide the immediate actionable item for a software engineer, in a single sentence. + Do not ask the user to check or look up information for themselves, that's your role; do your best to be informative. + """, + tools=[google_search], +) + +google_search_grounding = AgentTool(agent=_search_agent) diff --git a/contributing/samples/software_engineer_gemini/tools/shell_command.py b/contributing/samples/software_engineer_gemini/tools/shell_command.py new file mode 100644 index 0000000000..534cb52f99 --- /dev/null +++ b/contributing/samples/software_engineer_gemini/tools/shell_command.py @@ -0,0 +1,405 @@ +import logging +import shlex +import shutil # <-- Added import +import subprocess +from typing import Literal, Optional + +# Import ToolContext for state management +from google.adk.tools import ( + FunctionTool, # Ensure FunctionTool is imported if not already + ToolContext, +) +from pydantic import BaseModel, Field + +logger = logging.getLogger(__name__) + +# --- Configuration Tool --- # + + +class ConfigureShellApprovalInput(BaseModel): + """Input model for configuring shell command approval.""" + + require_approval: bool = Field(..., description="Set to true to require approval, false to disable.") + + +class ConfigureShellApprovalOutput(BaseModel): + """Output model for configuring shell command approval.""" + + status: str + + +def configure_shell_approval(args: dict, tool_context: ToolContext) -> ConfigureShellApprovalOutput: + """Configures whether running shell commands requires user approval for the current session. + + Args: + args (dict): A dictionary containing: + require_approval (bool): Set to true to require approval, false to disable. + tool_context (ToolContext): The context for accessing session state. + """ + require_approval = args.get("require_approval") + + # Add validation for the boolean argument + if require_approval is None or not isinstance(require_approval, bool): + message = "Error: 'require_approval' argument is missing or not a boolean (true/false)." + logger.error(message) + return ConfigureShellApprovalOutput(status=message) + + tool_context.state["require_shell_approval"] = require_approval + status = "enabled" if require_approval else "disabled" + logger.info(f"Shell command approval requirement set to: {status}") + return ConfigureShellApprovalOutput(status=f"Shell command approval requirement is now {status}.") + + +# --- Whitelist Configuration Tool --- # + + +class ConfigureShellWhitelistInput(BaseModel): + """Input model for configuring the shell command whitelist.""" + + action: Literal["add", "remove", "list", "clear"] = Field(..., description="Action to perform: add, remove, list, or clear.") + command: Optional[str] = Field(None, description="The command to add or remove (required for 'add' and 'remove' actions).") + + +class ConfigureShellWhitelistOutput(BaseModel): + """Output model for configuring the shell command whitelist.""" + + status: str + whitelist: Optional[list[str]] = Field(None, description="The current whitelist (only for 'list' action).") + + +def configure_shell_whitelist(args: dict, tool_context: ToolContext) -> ConfigureShellWhitelistOutput: + """Manages the whitelist of shell commands that bypass approval. + + Args: + args (dict): A dictionary containing: + action (Literal["add", "remove", "list", "clear"]): The action. + command (Optional[str]): The command for add/remove. + tool_context (ToolContext): The context for accessing session state. + """ + action = args.get("action") + command = args.get("command") + + # Default safe commands (adjust as needed) + DEFAULT_SAFE_COMMANDS = [ + "ls", + "grep", + "find", + "cat", + "pwd", + "echo", + "git status", + "head", + "tail", + "wc", + "git diff", + "git log", + "which", + "ping", + "host", + "dig", + "nslookup", + "ss", + "uname", + "uptime", + "date", + "df", + "du", + "free", + "stat", + "ps", + "pgrep", + "ip addr", + "ip route", + "traceroute", + "git grep", + "git branch", + "git branch --show-current", # Specific safe variant + "git tag", + "git remote -v", + "git config --list", + "docker ps", + "docker images", + "kubectl get", + "kubectl describe", + "kubectl logs", + "kubectl cluster-info", + "kubectl config view", + "kubectl version", + "kubectl api-resources", + "kubectl api-versions", + "kubectl top", + ] + + # Initialize whitelist in state if it doesn't exist + if "shell_command_whitelist" not in tool_context.state: + # Initialize with default safe commands + tool_context.state["shell_command_whitelist"] = DEFAULT_SAFE_COMMANDS[:] + logger.info(f"Initialized shell command whitelist with defaults: {DEFAULT_SAFE_COMMANDS}") + + whitelist: list[str] = tool_context.state["shell_command_whitelist"] + + if action == "add": + if not command: + return ConfigureShellWhitelistOutput(status="Error: 'command' is required for 'add' action.") + if command not in whitelist: + whitelist.append(command) + tool_context.state["shell_command_whitelist"] = whitelist # Update state + logger.info(f"Added command '{command}' to shell whitelist.") + return ConfigureShellWhitelistOutput(status=f"Command '{command}' added to whitelist.") + else: + return ConfigureShellWhitelistOutput(status=f"Command '{command}' is already in the whitelist.") + elif action == "remove": + if not command: + return ConfigureShellWhitelistOutput(status="Error: 'command' is required for 'remove' action.") + if command in whitelist: + whitelist.remove(command) + tool_context.state["shell_command_whitelist"] = whitelist # Update state + logger.info(f"Removed command '{command}' from shell whitelist.") + return ConfigureShellWhitelistOutput(status=f"Command '{command}' removed from whitelist.") + else: + return ConfigureShellWhitelistOutput(status=f"Command '{command}' not found in whitelist.") + elif action == "list": + return ConfigureShellWhitelistOutput(status="Current whitelist retrieved.", whitelist=list(whitelist)) # Return a copy + elif action == "clear": + tool_context.state["shell_command_whitelist"] = [] + logger.info("Cleared shell command whitelist.") + return ConfigureShellWhitelistOutput(status="Shell command whitelist cleared.") + else: + return ConfigureShellWhitelistOutput(status=f"Error: Invalid action '{action}'. Valid actions are: add, remove, list, clear.") + + +# --- Check Command Existence Tool --- # <--- Added section start + + +class CheckCommandExistsInput(BaseModel): + """Input model for checking command existence.""" + + command: str = Field(..., description="The command name (e.g., 'git', 'ls') to check for existence.") + + +class CheckCommandExistsOutput(BaseModel): + """Output model for checking command existence.""" + + exists: bool + command_checked: str + message: str + + +def check_command_exists(args: dict, tool_context: ToolContext) -> CheckCommandExistsOutput: + """Checks if a command exists in the system's PATH. Extracts the base command.""" + command_name = args.get("command") + base_command = None + message = "" + + if not command_name: + message = "Error: 'command' argument is missing." + logger.error(message) + return CheckCommandExistsOutput(exists=False, command_checked=command_name or "", message=message) + + try: + # Extract base command if it includes arguments (shutil.which needs the command name only) + parts = shlex.split(command_name) + if parts: + base_command = parts[0] + else: + message = f"Could not parse base command from input: '{command_name}'" + logger.warning(message) + return CheckCommandExistsOutput(exists=False, command_checked=command_name, message=message) + + except ValueError as e: + message = f"Error parsing command '{command_name}': {e}" + logger.error(message) + return CheckCommandExistsOutput(exists=False, command_checked=command_name, message=message) + + if not base_command: # Should not happen if parsing worked, but check anyway + message = "Error: Could not determine base command." + logger.error(message) + return CheckCommandExistsOutput(exists=False, command_checked=command_name, message=message) + + exists = shutil.which(base_command) is not None + status_msg = "exists" if exists else "does not exist" + message = f"Command '{base_command}' {status_msg} in system PATH." + logger.info(f"Checked existence for command '{base_command}': {exists}") + return CheckCommandExistsOutput(exists=exists, command_checked=base_command, message=message) + + +# <--- Added section end + + +# --- Shell Command Safety Check Tool --- # + + +class CheckShellCommandSafetyInput(BaseModel): + """Input model for checking shell command safety.""" + + command: str = Field(..., description="The shell command to check.") + + +class CheckShellCommandSafetyOutput(BaseModel): + """Output model for checking shell command safety.""" + + status: Literal["whitelisted", "approval_disabled", "approval_required"] = Field(..., description="The safety status of the command.") + command: str = Field(..., description="The command that was checked.") + message: str = Field(..., description="Explanation of the status.") + + +def check_shell_command_safety(args: dict, tool_context: ToolContext) -> CheckShellCommandSafetyOutput: + """Checks if a shell command is safe to run without explicit user approval. + + Checks against the configured whitelist and the session's approval requirement. + Does NOT execute the command. + + Args: + args (dict): A dictionary containing: + command (str): The shell command to check. + tool_context (ToolContext): The context for accessing session state. + + Returns: + CheckShellCommandSafetyOutput: An object indicating the safety status. + """ + command = args.get("command") + if not command: + # Technically this shouldn't happen with Pydantic validation, but belt-and-suspenders + return CheckShellCommandSafetyOutput( + status="approval_required", # Default to safest option on error + command=command or "", + message="Error: Command argument missing in input.", + ) + + require_approval = tool_context.state.get("require_shell_approval", True) + # Ensure whitelist is initialized if needed (accessing it via configure_shell_whitelist initializes) + if "shell_command_whitelist" not in tool_context.state: + # Temporarily call configure_shell_whitelist with 'list' action to initialize state + # This is a slight workaround to ensure initialization happens if only check/execute are called. + # A cleaner approach might involve a dedicated initialization step or context manager. + _ = configure_shell_whitelist({"action": "list"}, tool_context) + + shell_whitelist = tool_context.state.get("shell_command_whitelist", []) + is_whitelisted = command in shell_whitelist + + if is_whitelisted: + logger.info(f"Command '{command}' is whitelisted.") + return CheckShellCommandSafetyOutput(status="whitelisted", command=command, message="Command is in the configured whitelist and can be run directly.") + elif not require_approval: + logger.info(f"Command '{command}' is not whitelisted, but shell approval is disabled.") + return CheckShellCommandSafetyOutput( + status="approval_disabled", command=command, message="Command is not whitelisted, but approval is disabled for this session." + ) + else: + logger.warning(f"Command '{command}' requires approval (not whitelisted and approval enabled).") + return CheckShellCommandSafetyOutput( + status="approval_required", command=command, message="Command requires user approval as it is not whitelisted and approval is enabled." + ) + + +# --- Vetted Shell Command Execution Tool --- # + + +class ExecuteVettedShellCommandInput(BaseModel): + """Input model for the execute_vetted_shell_command tool.""" + + command: str = Field(..., description="The shell command to execute. Should have been vetted first.") + working_directory: Optional[str] = Field(None, description="Optional working directory to run the command in.") + timeout: int = Field(60, description="Timeout in seconds for the command execution.") + + +class ExecuteVettedShellCommandOutput(BaseModel): + """Output model for the execute_vetted_shell_command tool.""" + + stdout: str | None = Field(None, description="The standard output of the command.") + stderr: str | None = Field(None, description="The standard error of the command.") + return_code: int | None = Field(None, description="The return code of the command.") + command_executed: str | None = Field(None, description="The command that was executed.") + status: str = Field(description="Status: 'executed' or 'error'.") + message: str = Field(description="Additional information about the status.") + + +def execute_vetted_shell_command(args: dict, tool_context: ToolContext) -> ExecuteVettedShellCommandOutput: + """Executes a shell command that has ALREADY BEEN VETTED or explicitly approved. + + ***WARNING:*** DO NOT CALL THIS TOOL directly unless you have either: + 1. Called `check_shell_command_safety` and received a status of 'whitelisted' or 'approval_disabled'. + 2. Received explicit user confirmation to run this specific command. + + This tool performs NO safety checks itself. + + Args: + args (dict): A dictionary containing: + command (str): The shell command to execute. + working_directory (Optional[str]): Optional working directory. + timeout (Optional[int]): Optional timeout in seconds (default: 60). + tool_context (ToolContext): The context (unused here, but required by ADK). + + Returns: + ExecuteVettedShellCommandOutput: The result of the command execution. + """ + command = args.get("command") + working_directory = args.get("working_directory") + timeout = args.get("timeout", 60) + + if not command: + return ExecuteVettedShellCommandOutput(status="error", command_executed=command, message="Error: 'command' argument is missing.") + + try: + timeout_sec = int(timeout) + except (ValueError, TypeError): + return ExecuteVettedShellCommandOutput( + status="error", command_executed=command, message=f"Error: Invalid timeout value '{timeout}'. Must be an integer." + ) + + command_parts = shlex.split(command) + logger.info(f"Executing vetted shell command: '{command}' in directory '{working_directory or '.'}'") + + try: + process = subprocess.run( + command_parts, + capture_output=True, + text=True, + cwd=working_directory, + timeout=timeout_sec, + check=False, # Don't raise exception on non-zero exit + ) + logger.info(f"Vetted command '{command}' finished with return code {process.returncode}") + return ExecuteVettedShellCommandOutput( + stdout=process.stdout.strip(), + stderr=process.stderr.strip(), + return_code=process.returncode, + command_executed=command, + status="executed", + message="Command executed successfully." if process.returncode == 0 else "Command executed with non-zero exit code.", + ) + except FileNotFoundError: + logger.error(f"Command not found during execution: {command_parts[0]}") + return ExecuteVettedShellCommandOutput( + stderr=f"Error: Command not found: {command_parts[0]}", + return_code=-1, # Using distinct negative codes for different errors + command_executed=command, + status="error", + message=f"Command not found: {command_parts[0]}", + ) + except subprocess.TimeoutExpired: + logger.error(f"Vetted command '{command}' timed out after {timeout_sec} seconds.") + return ExecuteVettedShellCommandOutput( + stderr=f"Error: Command timed out after {timeout_sec} seconds.", + return_code=-2, + command_executed=command, + status="error", + message=f"Command timed out after {timeout_sec} seconds.", + ) + except Exception as e: + logger.exception(f"An unexpected error occurred while running vetted command '{command}': {e}") + return ExecuteVettedShellCommandOutput( + stderr=f"An unexpected error occurred: {e}", return_code=-3, command_executed=command, status="error", message=f"An unexpected error occurred: {e}" + ) + + +# --- Tool Registrations --- # <-- Added section (optional but good practice) + +# Wrap functions with FunctionTool +# Note: This assumes FunctionTool is imported or available in the scope + +configure_shell_approval_tool = FunctionTool(configure_shell_approval) +configure_shell_whitelist_tool = FunctionTool(configure_shell_whitelist) +check_command_exists_tool = FunctionTool(check_command_exists) # <-- Added tool +check_shell_command_safety_tool = FunctionTool(check_shell_command_safety) +execute_vetted_shell_command_tool = FunctionTool(execute_vetted_shell_command) diff --git a/contributing/samples/software_engineer_gemini/tools/system_info.py b/contributing/samples/software_engineer_gemini/tools/system_info.py new file mode 100644 index 0000000000..7e236e4f4e --- /dev/null +++ b/contributing/samples/software_engineer_gemini/tools/system_info.py @@ -0,0 +1,31 @@ +import logging +import platform + +from google.adk.tools import FunctionTool +from pydantic import BaseModel, Field + +logger = logging.getLogger(__name__) + + +class OSInfoOutput(BaseModel): + """Output model for the get_os_info tool.""" + + system: str = Field(description="Operating system name (e.g., 'Linux', 'Darwin', 'Windows').") + release: str = Field(description="Operating system release (e.g., '5.15.0-78-generic').") + version: str = Field(description="Operating system version.") + machine: str = Field(description="Machine hardware name (e.g., 'x86_64').") + + +def get_os_info() -> OSInfoOutput: + """Gets basic operating system information.""" + logger.info("Getting operating system information.") + try: + return OSInfoOutput(system=platform.system(), release=platform.release(), version=platform.version(), machine=platform.machine()) + except Exception as e: + logger.exception(f"Failed to get OS info: {e}") + # Return placeholder values on error + return OSInfoOutput(system="Unknown", release="Unknown", version="Unknown", machine="Unknown") + + +# Wrap get_os_info with FunctionTool +get_os_info_tool = FunctionTool(get_os_info) diff --git a/contributing/samples/software_engineer_gemini_mcp/IMPROVEMENTS.md b/contributing/samples/software_engineer_gemini_mcp/IMPROVEMENTS.md new file mode 100644 index 0000000000..2abc4de442 --- /dev/null +++ b/contributing/samples/software_engineer_gemini_mcp/IMPROVEMENTS.md @@ -0,0 +1,241 @@ +# Agent Improvement Log + +This document tracks potential improvements for the multi-agent system. + +## Root Agent (`root_agent`) + +**Goal:** Maximize delegation to sub-agents; root agent should primarily orchestrate and delegate, performing minimal direct task execution. + +**Potential Improvements:** + +1. **Tool Rationalization:** + * **Current State:** The root agent possesses a wide array of tools (filesystem, shell execution, code search, etc.). + * **Observation:** Sub-agents (e.g., `devops_agent`) also have many of these tools. + * **Suggestion:** + - [x] Review tool duplication. (Addressed for shell, edit, code_search, os_info tools) + - [x] Consider if the root agent truly needs all these tools if its primary role is delegation. Could it rely on sub-agents for most tool-based actions? (Addressed for shell, edit, code_search, os_info tools) + * **Action:** + - [x] Identify tools that can be exclusively managed by sub-agents. The root agent might only need delegation tools and perhaps very minimal interaction/utility tools. (Addressed for shell, edit, code_search, os_info tools) + +2. **Prompt Focus:** + * **Current State:** The root agent's prompt (`ROOT_AGENT_INSTR`) provides detailed instructions on *how* to use various tools (e.g., file editing workflow, shell command workflow). + * **Observation:** If sub-agents are responsible for tool execution, these detailed instructions might be more appropriate for sub-agent prompts. + * **Suggestion:** + - [x] Refine the root agent's prompt to focus more on *strategic delegation* (Significantly addressed by removing shell, edit, code_search, os_info tools and updating prompt) + * [x] Identifying the correct sub-agent for a task. (Prompt updated) + * [x] Passing necessary context to the sub-agent. (Implicit in delegation) + * [x] Handling responses from sub-agents. (Standard agent flow) + * [x] Less emphasis on the low-level mechanics of tool usage. (Prompt updated) + +3. **Shell Command Tools (as per existing TODO):** + * **Current State:** The root agent's `agent.py` has a TODO: "Move command tools to devops_agent with more guardrails." + * **Suggestion:** + - [x] Prioritize this. Centralizing shell command execution within the `devops_agent` (which has a strong safety focus) seems appropriate. The root agent could then request shell operations *through* the `devops_agent`. + +4. **Memory Tool Access:** + * **Current State:** The root agent has `load_memory`, `add_memory_fact`, `search_memory_facts` operating on a global memory store. + * **Observation:** Clarity was needed on how sub-agents interact with memory. + * **Strategy Defined:** + - [x] Define a clear strategy for memory access across agents. + * **Global Project Context:** The root agent manages a global `project_context` (using its memory tools) and passes it as read-only information to sub-agents upon invocation. This provides foundational knowledge. (Aligns with item 5). + * **Sub-Agent Local Memory:** Sub-agents performing complex, multi-step tasks can be equipped with the *same memory tool functions* (e.g., `add_memory_fact`, + `search_memory_facts`). However, for sub-agents, these tools will operate on a **separate, isolated, and potentially ephemeral local memory store**, specific to their current task. This + allows them to manage intermediate findings without affecting global memory. + * **Promoting Information to Global Memory:** If a sub-agent discovers information of lasting global importance (that should update the `project_context` or shared knowledge) + it should report this back to the root agent as part of its results. The root agent then uses its memory tools to add this information to the global memory store. This maintains centralized + control and curation of shared knowledge. + * **Summary of Access & Tooling:** + * Root Agent: Owns global memory tools for the main `project_context`. + * Sub-Agents: + * Receive global `project_context` (read-only). + * Can be equipped with identical memory tool *functions* that are scoped to operate on their own *local* memory store for task-specific needs. + * Request updates to global memory via the root agent (by returning facts/insights). + +5. **Project Context (`load_project_context`):** + * **Current State:** The `load_project_context` callback loads context into the root agent's state. + * **Suggestion:** + - [x] Ensure this project context is effectively passed down to sub-agents when they are invoked, so they have the necessary background information. + - [x] Verify the mechanism (current prompts suggest this is happening with `{project_context}` placeholders). + +## DevOps Agent (`devops_agent`) + +**Goal:** Effectively handle DevOps, CI/CD, deployment, and infrastructure tasks using its specialized tools and knowledge. + +**Potential Improvements:** + +1. **MCP Toolset Clarity:** + * **Current State:** Uses `MCPToolset` with `npx -y @modelcontextprotocol/server-filesystem`. + * **Observation:** The exact capabilities and limitations of this `server-filesystem` via MCP aren't fully clear from the code alone. The commented-out `tool_predicate` suggests an awareness of restricting write operations. + * **Suggestion:** + - [x] Ensure the agent's prompt or internal logic clearly understands when to use these MCP-based file tools versus the standard ADK file tools (`read_file_tool`, `edit_file_tool`). + - [x] Clarify if the MCP toolset offers advantages (e.g., performance for large files, specific metadata access) that would guide this choice. + - [x] If the `tool_predicate` for restricting writes is desired, it should be implemented. (ignore for now) + +2. **Tool Redundancy with Root Agent:** + * **Current State:** Has `read_file_tool`, `list_dir_tool`, `edit_file_tool`, `codebase_search_tool`, `execute_vetted_shell_command_tool`, `check_command_exists_tool`. + * **Observation:** These are also present in the root agent. + * **Suggestion:** + - [x] As part of the broader tool rationalization, confirm if the `devops_agent` should be the primary owner/user of shell command tools. If so, the root agent might not need them directly. (Addressed by moving shell tools to devops_agent) + +3. **Internal `_search_agent` and `_code_execution_agent` Prompts:** + * **Current State:** These internal agents have their own specific prompts. + * **Suggestion:** + - [x] Periodically review these prompts to ensure they remain aligned with the `devops_agent`'s overall goals. The `_search_agent`'s instruction for "brief but concise" single-sentence actionable items might be too limiting for complex research tasks the DevOps agent might need. + +4. **Environment Variable Dependency (`MCP_ALLOWED_DIRECTORIES`):** + * **Current State:** Relies on `.env` and `MCP_ALLOWED_DIRECTORIES`. + * **Suggestion:** + - [x] Ensure clear documentation for setting up these environment variables. (Fallback exists, which is good). (Documentation added to IMPROVEMENTS.md) + +## Code Quality Agent (`code_quality_agent`) + +**Goal:** Provide expert analysis of code quality, identify issues, and suggest actionable improvements. + +**Potential Improvements:** + +1. **Integration with `edit_file_tool` (Optional/Consideration):** + * **Current State:** Suggests fixes. + * **Suggestion:** + - [IN_PROGRESS: Awaiting code quality feedback. Code changes submitted to code_quality_agent for review.] Consider if there are scenarios where, after user approval, this agent *could* apply straightforward, safe fixes using `edit_file_tool`. This would enhance its capability but requires careful safety considerations. Perhaps it proposes an edit, and the user or another agent applies it. + +2. **Clarity on `analyze_code_tool`'s Capabilities:** + * **Observation:** The prompt mentions analyzing for various issues (bugs, smells, security, etc.). + * **Suggestion:** + - [ ] Document (for developers/maintainers) what specific linters or analyzers `analyze_code_tool` uses internally. This helps understand its scope and limitations. + +3. **Batch Analysis/Project-Wide Analysis:** + * **Current State:** Prompt implies analysis of a "specified file." + * **Suggestion:** + - [ ] Evaluate if the agent should support project-wide or directory-wide analysis. This might need enhancements to `analyze_code_tool` or a more complex interaction pattern for the agent. + +## Code Review Agent (`code_review_agent`) + +**Goal:** Perform meticulous, deep code reviews, identify a wide range of issues, and provide high-quality, actionable feedback. + +**Potential Improvements:** + +1. **Tool Alignment with Prompt (Critical):** + * **Current State:** The agent's `tools` list in `agent.py` is missing several tools (`check_command_exists_tool`, `codebase_search_tool`, shell command tools) that its prompt explicitly instructs it to use. + * **Suggestion:** + - [x] Add the necessary tools to `code_review_agent`'s toolset to enable its documented workflow. This is a high-priority fix. + * **Action:** + - [x] Add `check_command_exists_tool`, `codebase_search_tool`, `check_shell_command_safety_tool`, `execute_vetted_shell_command_tool`. + - [x] Consider `configure_shell_approval_tool` and `configure_shell_whitelist_tool`. (Marked as done as they were added based on previous check) + +2. **Clarify Role of `analyze_code_tool`:** + * **Current State:** Has `analyze_code_tool`. The prompt emphasizes manual review and discovery/execution of external tools. + * **Suggestion:** + - [ ] Refine the prompt to clarify how `analyze_code_tool` fits into its workflow. Is it a primary analysis engine, a fallback, or a supplement to externally run tools? + +3. **Max Output Tokens:** + * **Current State:** `max_output_tokens=1000`. + * **Suggestion:** + - [ ] Monitor if this is sufficient for detailed code reviews, which can be lengthy. + * Consider increasing if truncation is observed. + +4. **Structured Output (Revisit `CodeReviewResponse`):** + * **Current State:** A commented-out import for `CodeReviewResponse` exists. + * **Suggestion:** + - [ ] For complex reviews, a structured response type could improve consistency and programmatic usability. (Lower priority). + +## Debugging Agent (`debugging_agent`) + +**Goal:** Systematically find and fix bugs by analyzing code, errors, and context. + +**Potential Improvements:** + +1. **Interactive Debugging Tool (Future Consideration):** + * **Current State:** Relies on static analysis and command execution. + * **Suggestion:** + - [ ] For complex scenarios, an interactive debugger tool (set breakpoints, inspect variables) would be a powerful but complex future enhancement. + +2. **State Management for Multi-Step Debugging:** + * **Observation:** Debugging can be iterative. + * **Suggestion:** + - [ ] Encourage through prompt or examples how the agent should maintain state/memory of previous attempts and hypotheses during a complex debugging session. + +3. **Clarity on `get_os_info` Tool:** + * **Observation:** Uses `get_os_info`. + * **Suggestion:** + - [ ] Ensure this tool provides OS information most relevant for debugging common software issues (e.g., OS version, kernel, key environment variables, CPU architecture). + +## Design Pattern Agent (`design_pattern_agent`) + +**Goal:** Analyze codebases and recommend appropriate design patterns or architectural improvements. + +**Potential Improvements:** + +1. **Clarify Role of `execute_vetted_shell_command_tool`:** + * **Current State:** Tool is present, but prompt doesn't strongly guide its usage. + * **Suggestion:** + - [ ] If there are specific scenarios where shell commands are useful (e.g., invoking code generation tools for patterns), add hints to the prompt. Otherwise, consider its necessity for core tasks. + +2. **Integration with Code Generation/Refactoring Tools (Advanced):** + * **Current State:** Can generate code snippets/full files for `edit_file_tool`. + * **Suggestion:** + - [ ] Explore integration with automated refactoring or pattern implementation tools (e.g., IDE refactoring tools via CLI) for more advanced capabilities. (Future consideration). + +3. **Knowledge Base of Patterns (Internal vs. External Search):** + * **Current State:** Relies on training and `google_search_grounding`. + * **Suggestion:** + - [ ] Guide the agent to balance its internal knowledge with external search effectively. + - [ ] Ensure robust internal knowledge of common patterns. + +## Documentation Agent (`documentation_agent`) + +**Goal:** Generate clear, accurate, and comprehensive documentation for code, APIs, and projects. + +**Potential Improvements:** + +1. **Tool Alignment for Shell Workflow (Important):** + * **Current State:** Prompt instructs use of `check_command_exists_tool` and `check_shell_command_safety_tool` for running doc generators, but these are missing from `agent.py` (though `execute_vetted_shell_command_tool` is present). + * **Suggestion:** + - [x] Add `check_command_exists_tool` and `check_shell_command_safety_tool` to its toolset for the full documented shell workflow. + +2. **Clarify/Correct `file_search` Tool Mention:** + * **Current State:** Prompt mentions a `file_search` tool in its latter section, which is not a defined tool. + * **Suggestion:** + - [x] If this is a typo for `codebase_search_tool`, correct it. + - [ ] If `file_search` is a distinct, intended tool, it needs to be implemented and added. + +3. **Docstring/Comment Insertion Precision:** + * **Current State:** Uses `edit_file_content` to insert docstrings/comments. + * **Suggestion:** + - [ ] (In Progress) Inserting into existing code precisely can be hard with a tool that replaces entire files (like the ADK `edit_file_content` seems to be). Consider if a more precise line-based editing tool (like the other `edit_file` we saw) is needed, or if the agent should only generate docstrings for the user to insert manually. + +4. **Awareness of Documentation Formats/Styles:** + * **Current State:** Prompt mentions researching standards. + * **Suggestion:** + - [ ] Reinforce through examples or prompt details the expected quality and conventions for common documentation types (Markdown, Python docstrings, Javadoc, etc.). + +## Testing Agent (`testing_agent`) + +**Goal:** Help developers create comprehensive and effective automated tests. + +**Potential Improvements:** + +1. **Tool Alignment for Test Execution Workflow (Critical):** + * **Current State:** Prompt mandates a workflow involving `check_command_exists_tool` and `check_shell_command_safety_tool` before running tests, but these tools are missing from `agent.py`. + * **Suggestion:** + - [x] Add `check_command_exists_tool` and `check_shell_command_safety_tool` to its toolset. + - [x] Consider `configure_shell_approval_tool` and `configure_shell_whitelist_tool` for managing test execution settings. + * **Action:** + - [x] This is critical for the agent to reliably and safely discover and execute project-specific test commands. (Marked as done as suggestions cover this) + +2. **Guidance on Test Framework Interaction:** + * **Current State:** General instructions to generate tests. + * **Suggestion:** + - [ ] Enhance the prompt with examples or guidance on structuring tests for common frameworks (e.g., pytest, Jest conventions) to improve the quality of generated tests. + +3. **Coverage Analysis Interpretation:** + * **Current State:** Mentions running coverage tools. + * **Suggestion:** + - [ ] If the agent is expected to interpret coverage reports (not just run the tool), it might need more guidance or a specialized tool for parsing common coverage report formats. For now, running the tool and presenting output is a good first step. + +4. **Mocking/Stubbing Assistance:** + * **Current State:** Mentions employing mocking. + * **Suggestion:** + - [ ] Prompt the agent to ask for clarification on dependencies to mock or to use `google_search_grounding` for framework-specific mocking examples. + + +Now that we have a working implementation with mcp tools for working with files in the devops agent, could we possibly reuse the same approach including the fallback for all agents that need to do file operations? Rather than copy/paste, it would be nice to have shared code for this right? Many of our sub agents will likely need these tools otherwise they will always have to delegate/transfer to devops agent which seems like we are wasting compute cycles. \ No newline at end of file diff --git a/contributing/samples/software_engineer_gemini_mcp/__init__.py b/contributing/samples/software_engineer_gemini_mcp/__init__.py new file mode 100644 index 0000000000..d9ec974bc2 --- /dev/null +++ b/contributing/samples/software_engineer_gemini_mcp/__init__.py @@ -0,0 +1,10 @@ +# ruff: noqa +""" +Software Engineer Agent. + +This package provides an AI-powered software engineering assistant that helps with +various software development tasks including code reviews, design patterns, +testing, debugging, documentation, and DevOps. +""" + +from . import agent diff --git a/contributing/samples/software_engineer_gemini_mcp/agent-improvements.session.json b/contributing/samples/software_engineer_gemini_mcp/agent-improvements.session.json new file mode 100644 index 0000000000..ffce84c3bf --- /dev/null +++ b/contributing/samples/software_engineer_gemini_mcp/agent-improvements.session.json @@ -0,0 +1,530 @@ +{ + "id": "0e116cbb-a3b0-49ed-b56e-f4ed6256a8c3", + "app_name": "software_engineer_gemini_mcp", + "user_id": "test_user", + "state": { + "project_context": "{}", + "user_profile": "{}", + "task_management_output": "For context:[documentation_agent] I am the documentation_agent, I have received the task 'DevOps Agent - Document MCP_ALLOWED_DIRECTORIES' for the devops_agent.\n\nPlease create documentation for the `MCP_ALLOWED_DIRECTORIES` environment variable, used by the devops_agent. Explain its setup and mention the existing fallback mechanism. Update the task in IMPROVEMENTS.md upon completion by marking it as [x] and adding a note about the documentation location. \n", + "software_engineer": "For context:[documentation_agent] I am the documentation_agent, I have received the task 'DevOps Agent - Document MCP_ALLOWED_DIRECTORIES' for the devops_agent.\n\nPlease create documentation for the `MCP_ALLOWED_DIRECTORIES` environment variable, used by the devops_agent. Explain its setup and mention the existing fallback mechanism. Update the task in IMPROVEMENTS.md upon completion by marking it as [x] and adding a note about the documentation location. \n", + "documentation": "For context:[documentation_agent] I am the documentation_agent, I have received the task 'DevOps Agent - Document MCP_ALLOWED_DIRECTORIES' for the devops_agent.\n\nPlease create documentation for the `MCP_ALLOWED_DIRECTORIES` environment variable, used by the devops_agent. Explain its setup and mention the existing fallback mechanism. Update the task in IMPROVEMENTS.md upon completion by marking it as [x] and adding a note about the documentation location. \n" + }, + "events": [ + { + "content": { + "parts": [ + { + "text": "what is our next task from '/Users/james/Workspace/gh/lab/adk-python/contributing/samples/software_engineer_gemini_mcp/IMPROVEMENTS.md'" + } + ], + "role": "user" + }, + "invocation_id": "e-c8e2b1da-5a77-490a-b6d4-d5d4cbff1536", + "author": "user", + "actions": { + "state_delta": {}, + "artifact_delta": {}, + "requested_auth_configs": {} + }, + "id": "C5hf2Zww", + "timestamp": 1746990740.478392 + }, + { + "invocation_id": "e-c8e2b1da-5a77-490a-b6d4-d5d4cbff1536", + "author": "root_agent", + "actions": { + "state_delta": { + "project_context": "{}", + "user_profile": "{}" + }, + "artifact_delta": {}, + "requested_auth_configs": {} + }, + "id": "0z4XHWue", + "timestamp": 1746990740.478704 + }, + { + "content": { + "parts": [ + { + "function_call": { + "id": "adk-6767f699-d82c-4036-9262-d70ee113590f", + "args": { + "agent_name": "task_management_agent" + }, + "name": "transfer_to_agent" + } + } + ], + "role": "model" + }, + "invocation_id": "e-c8e2b1da-5a77-490a-b6d4-d5d4cbff1536", + "author": "root_agent", + "actions": { + "state_delta": {}, + "artifact_delta": {}, + "requested_auth_configs": {} + }, + "long_running_tool_ids": [], + "id": "Pse839OB", + "timestamp": 1746990740.47981 + }, + { + "content": { + "parts": [ + { + "function_response": { + "id": "adk-6767f699-d82c-4036-9262-d70ee113590f", + "name": "transfer_to_agent", + "response": {} + } + } + ], + "role": "user" + }, + "invocation_id": "e-c8e2b1da-5a77-490a-b6d4-d5d4cbff1536", + "author": "root_agent", + "actions": { + "state_delta": {}, + "artifact_delta": {}, + "transfer_to_agent": "task_management_agent", + "requested_auth_configs": {} + }, + "id": "oE7U3s03", + "timestamp": 1746990741.942243 + }, + { + "content": { + "parts": [ + { + "function_call": { + "id": "adk-f6b27a38-2804-4143-be39-9f9ee2dce6b9", + "args": { + "tasks_file_path": "/Users/james/Workspace/gh/lab/adk-python/contributing/samples/software_engineer_gemini_mcp/IMPROVEMENTS.md" + }, + "name": "list_all_tasks_markdown" + } + } + ], + "role": "model" + }, + "invocation_id": "e-c8e2b1da-5a77-490a-b6d4-d5d4cbff1536", + "author": "task_management_agent", + "actions": { + "state_delta": {}, + "artifact_delta": {}, + "requested_auth_configs": {} + }, + "long_running_tool_ids": [], + "id": "hSmF3fzO", + "timestamp": 1746990741.944137 + }, + { + "content": { + "parts": [ + { + "function_response": { + "id": "adk-f6b27a38-2804-4143-be39-9f9ee2dce6b9", + "name": "list_all_tasks_markdown", + "response": { + "result": "# Agent Improvement Log\n\nThis document tracks potential improvements for the multi-agent system.\n\n## Root Agent (`root_agent`)\n\n**Goal:** Maximize delegation to sub-agents; root agent should primarily orchestrate and delegate, performing minimal direct task execution.\n\n**Potential Improvements:**\n\n1. **Tool Rationalization:**\n * **Current State:** The root agent possesses a wide array of tools (filesystem, shell execution, code search, etc.).\n * **Observation:** Sub-agents (e.g., `devops_agent`) also have many of these tools.\n * **Suggestion:**\n - [x] Review tool duplication. (Addressed for shell, edit, code_search, os_info tools)\n - [x] Consider if the root agent truly needs all these tools if its primary role is delegation. Could it rely on sub-agents for most tool-based actions? (Addressed for shell, edit, code_search, os_info tools)\n * **Action:**\n - [x] Identify tools that can be exclusively managed by sub-agents. The root agent might only need delegation tools and perhaps very minimal interaction/utility tools. (Addressed for shell, edit, code_search, os_info tools)\n\n2. **Prompt Focus:**\n * **Current State:** The root agent's prompt (`ROOT_AGENT_INSTR`) provides detailed instructions on *how* to use various tools (e.g., file editing workflow, shell command workflow).\n * **Observation:** If sub-agents are responsible for tool execution, these detailed instructions might be more appropriate for sub-agent prompts.\n * **Suggestion:**\n - [x] Refine the root agent's prompt to focus more on *strategic delegation* (Significantly addressed by removing shell, edit, code_search, os_info tools and updating prompt)\n * [x] Identifying the correct sub-agent for a task. (Prompt updated)\n * [x] Passing necessary context to the sub-agent. (Implicit in delegation)\n * [x] Handling responses from sub-agents. (Standard agent flow)\n * [x] Less emphasis on the low-level mechanics of tool usage. (Prompt updated)\n\n3. **Shell Command Tools (as per existing TODO):**\n * **Current State:** The root agent's `agent.py` has a TODO: \"Move command tools to devops_agent with more guardrails.\"\n * **Suggestion:**\n - [x] Prioritize this. Centralizing shell command execution within the `devops_agent` (which has a strong safety focus) seems appropriate. The root agent could then request shell operations *through* the `devops_agent`.\n\n4. **Memory Tool Access:**\n * **Current State:** The root agent has `load_memory`, `add_memory_fact`, `search_memory_facts` operating on a global memory store.\n * **Observation:** Clarity was needed on how sub-agents interact with memory.\n * **Strategy Defined:**\n - [x] Define a clear strategy for memory access across agents.\n * **Global Project Context:** The root agent manages a global `project_context` (using its memory tools) and passes it as read-only information to sub-agents upon invocation. This provides foundational knowledge. (Aligns with item 5).\n * **Sub-Agent Local Memory:** Sub-agents performing complex, multi-step tasks can be equipped with the *same memory tool functions* (e.g., `add_memory_fact`,\n `search_memory_facts`). However, for sub-agents, these tools will operate on a **separate, isolated, and potentially ephemeral local memory store**, specific to their current task. This\n allows them to manage intermediate findings without affecting global memory.\n * **Promoting Information to Global Memory:** If a sub-agent discovers information of lasting global importance (that should update the `project_context` or shared knowledge)\n it should report this back to the root agent as part of its results. The root agent then uses its memory tools to add this information to the global memory store. This maintains centralized\n control and curation of shared knowledge.\n * **Summary of Access & Tooling:**\n * Root Agent: Owns global memory tools for the main `project_context`.\n * Sub-Agents:\n * Receive global `project_context` (read-only).\n * Can be equipped with identical memory tool *functions* that are scoped to operate on their own *local* memory store for task-specific needs.\n * Request updates to global memory via the root agent (by returning facts/insights).\n\n5. **Project Context (`load_project_context`):**\n * **Current State:** The `load_project_context` callback loads context into the root agent's state.\n * **Suggestion:**\n - [x] Ensure this project context is effectively passed down to sub-agents when they are invoked, so they have the necessary background information.\n - [x] Verify the mechanism (current prompts suggest this is happening with `{project_context}` placeholders).\n\n## DevOps Agent (`devops_agent`)\n\n**Goal:** Effectively handle DevOps, CI/CD, deployment, and infrastructure tasks using its specialized tools and knowledge.\n\n**Potential Improvements:**\n\n1. **MCP Toolset Clarity:**\n * **Current State:** Uses `MCPToolset` with `npx -y @modelcontextprotocol/server-filesystem`.\n * **Observation:** The exact capabilities and limitations of this `server-filesystem` via MCP aren't fully clear from the code alone. The commented-out `tool_predicate` suggests an awareness of restricting write operations.\n * **Suggestion:**\n - [x] Ensure the agent's prompt or internal logic clearly understands when to use these MCP-based file tools versus the standard ADK file tools (`read_file_tool`, `edit_file_tool`).\n - [x] Clarify if the MCP toolset offers advantages (e.g., performance for large files, specific metadata access) that would guide this choice.\n - [x] If the `tool_predicate` for restricting writes is desired, it should be implemented. (ignore for now)\n\n2. **Tool Redundancy with Root Agent:**\n * **Current State:** Has `read_file_tool`, `list_dir_tool`, `edit_file_tool`, `codebase_search_tool`, `execute_vetted_shell_command_tool`, `check_command_exists_tool`.\n * **Observation:** These are also present in the root agent.\n * **Suggestion:**\n - [x] As part of the broader tool rationalization, confirm if the `devops_agent` should be the primary owner/user of shell command tools. If so, the root agent might not need them directly. (Addressed by moving shell tools to devops_agent)\n\n3. **Internal `_search_agent` and `_code_execution_agent` Prompts:**\n * **Current State:** These internal agents have their own specific prompts.\n * **Suggestion:**\n - [x] Periodically review these prompts to ensure they remain aligned with the `devops_agent`'s overall goals. The `_search_agent`'s instruction for \"brief but concise\" single-sentence actionable items might be too limiting for complex research tasks the DevOps agent might need.\n\n4. **Environment Variable Dependency (`MCP_ALLOWED_DIRECTORIES`):**\n * **Current State:** Relies on `.env` and `MCP_ALLOWED_DIRECTORIES`.\n * **Suggestion:**\n - [ ] Ensure clear documentation for setting up these environment variables. (Fallback exists, which is good).\n\n## Code Quality Agent (`code_quality_agent`)\n\n**Goal:** Provide expert analysis of code quality, identify issues, and suggest actionable improvements.\n\n**Potential Improvements:**\n\n1. **Integration with `edit_file_tool` (Optional/Consideration):**\n * **Current State:** Suggests fixes.\n * **Suggestion:**\n - [ ] Consider if there are scenarios where, after user approval, this agent *could* apply straightforward, safe fixes using `edit_file_tool`. This would enhance its capability but requires careful safety considerations. Perhaps it proposes an edit, and the user or another agent applies it.\n\n2. **Clarity on `analyze_code_tool`'s Capabilities:**\n * **Observation:** The prompt mentions analyzing for various issues (bugs, smells, security, etc.).\n * **Suggestion:**\n - [ ] Document (for developers/maintainers) what specific linters or analyzers `analyze_code_tool` uses internally. This helps understand its scope and limitations.\n\n3. **Batch Analysis/Project-Wide Analysis:**\n * **Current State:** Prompt implies analysis of a \"specified file.\"\n * **Suggestion:**\n - [ ] Evaluate if the agent should support project-wide or directory-wide analysis. This might need enhancements to `analyze_code_tool` or a more complex interaction pattern for the agent.\n\n## Code Review Agent (`code_review_agent`)\n\n**Goal:** Perform meticulous, deep code reviews, identify a wide range of issues, and provide high-quality, actionable feedback.\n\n**Potential Improvements:**\n\n1. **Tool Alignment with Prompt (Critical):**\n * **Current State:** The agent's `tools` list in `agent.py` is missing several tools (`check_command_exists_tool`, `codebase_search_tool`, shell command tools) that its prompt explicitly instructs it to use.\n * **Suggestion:**\n - [x] Add the necessary tools to `code_review_agent`'s toolset to enable its documented workflow. This is a high-priority fix.\n * **Action:**\n - [x] Add `check_command_exists_tool`, `codebase_search_tool`, `check_shell_command_safety_tool`, `execute_vetted_shell_command_tool`.\n - [x] Consider `configure_shell_approval_tool` and `configure_shell_whitelist_tool`. (Marked as done as they were added based on previous check)\n\n2. **Clarify Role of `analyze_code_tool`:**\n * **Current State:** Has `analyze_code_tool`. The prompt emphasizes manual review and discovery/execution of external tools.\n * **Suggestion:**\n - [ ] Refine the prompt to clarify how `analyze_code_tool` fits into its workflow. Is it a primary analysis engine, a fallback, or a supplement to externally run tools?\n\n3. **Max Output Tokens:**\n * **Current State:** `max_output_tokens=1000`.\n * **Suggestion:**\n - [ ] Monitor if this is sufficient for detailed code reviews, which can be lengthy.\n * Consider increasing if truncation is observed.\n\n4. **Structured Output (Revisit `CodeReviewResponse`):**\n * **Current State:** A commented-out import for `CodeReviewResponse` exists.\n * **Suggestion:**\n - [ ] For complex reviews, a structured response type could improve consistency and programmatic usability. (Lower priority).\n\n## Debugging Agent (`debugging_agent`)\n\n**Goal:** Systematically find and fix bugs by analyzing code, errors, and context.\n\n**Potential Improvements:**\n\n1. **Interactive Debugging Tool (Future Consideration):**\n * **Current State:** Relies on static analysis and command execution.\n * **Suggestion:**\n - [ ] For complex scenarios, an interactive debugger tool (set breakpoints, inspect variables) would be a powerful but complex future enhancement.\n\n2. **State Management for Multi-Step Debugging:**\n * **Observation:** Debugging can be iterative.\n * **Suggestion:**\n - [ ] Encourage through prompt or examples how the agent should maintain state/memory of previous attempts and hypotheses during a complex debugging session.\n\n3. **Clarity on `get_os_info` Tool:**\n * **Observation:** Uses `get_os_info`.\n * **Suggestion:**\n - [ ] Ensure this tool provides OS information most relevant for debugging common software issues (e.g., OS version, kernel, key environment variables, CPU architecture).\n\n## Design Pattern Agent (`design_pattern_agent`)\n\n**Goal:** Analyze codebases and recommend appropriate design patterns or architectural improvements.\n\n**Potential Improvements:**\n\n1. **Clarify Role of `execute_vetted_shell_command_tool`:**\n * **Current State:** Tool is present, but prompt doesn't strongly guide its usage.\n * **Suggestion:**\n - [ ] If there are specific scenarios where shell commands are useful (e.g., invoking code generation tools for patterns), add hints to the prompt. Otherwise, consider its necessity for core tasks.\n\n2. **Integration with Code Generation/Refactoring Tools (Advanced):**\n * **Current State:** Can generate code snippets/full files for `edit_file_tool`.\n * **Suggestion:**\n - [ ] Explore integration with automated refactoring or pattern implementation tools (e.g., IDE refactoring tools via CLI) for more advanced capabilities. (Future consideration).\n\n3. **Knowledge Base of Patterns (Internal vs. External Search):**\n * **Current State:** Relies on training and `google_search_grounding`.\n * **Suggestion:**\n - [ ] Guide the agent to balance its internal knowledge with external search effectively.\n - [ ] Ensure robust internal knowledge of common patterns.\n\n## Documentation Agent (`documentation_agent`)\n\n**Goal:** Generate clear, accurate, and comprehensive documentation for code, APIs, and projects.\n\n**Potential Improvements:**\n\n1. **Tool Alignment for Shell Workflow (Important):**\n * **Current State:** Prompt instructs use of `check_command_exists_tool` and `check_shell_command_safety_tool` for running doc generators, but these are missing from `agent.py` (though `execute_vetted_shell_command_tool` is present).\n * **Suggestion:**\n - [x] Add `check_command_exists_tool` and `check_shell_command_safety_tool` to its toolset for the full documented shell workflow.\n\n2. **Clarify/Correct `file_search` Tool Mention:**\n * **Current State:** Prompt mentions a `file_search` tool in its latter section, which is not a defined tool.\n * **Suggestion:**\n - [x] If this is a typo for `codebase_search_tool`, correct it.\n - [ ] If `file_search` is a distinct, intended tool, it needs to be implemented and added.\n\n3. **Docstring/Comment Insertion Precision:**\n * **Current State:** Uses `edit_file_content` to insert docstrings/comments.\n * **Suggestion:**\n - [ ] (In Progress) Inserting into existing code precisely can be hard with a tool that replaces entire files (like the ADK `edit_file_content` seems to be). Consider if a more precise line-based editing tool (like the other `edit_file` we saw) is needed, or if the agent should only generate docstrings for the user to insert manually.\n\n4. **Awareness of Documentation Formats/Styles:**\n * **Current State:** Prompt mentions researching standards.\n * **Suggestion:**\n - [ ] Reinforce through examples or prompt details the expected quality and conventions for common documentation types (Markdown, Python docstrings, Javadoc, etc.).\n\n## Testing Agent (`testing_agent`)\n\n**Goal:** Help developers create comprehensive and effective automated tests.\n\n**Potential Improvements:**\n\n1. **Tool Alignment for Test Execution Workflow (Critical):**\n * **Current State:** Prompt mandates a workflow involving `check_command_exists_tool` and `check_shell_command_safety_tool` before running tests, but these tools are missing from `agent.py`.\n * **Suggestion:**\n - [x] Add `check_command_exists_tool` and `check_shell_command_safety_tool` to its toolset.\n - [x] Consider `configure_shell_approval_tool` and `configure_shell_whitelist_tool` for managing test execution settings.\n * **Action:**\n - [x] This is critical for the agent to reliably and safely discover and execute project-specific test commands. (Marked as done as suggestions cover this)\n\n2. **Guidance on Test Framework Interaction:**\n * **Current State:** General instructions to generate tests.\n * **Suggestion:**\n - [ ] Enhance the prompt with examples or guidance on structuring tests for common frameworks (e.g., pytest, Jest conventions) to improve the quality of generated tests.\n\n3. **Coverage Analysis Interpretation:**\n * **Current State:** Mentions running coverage tools.\n * **Suggestion:**\n - [ ] If the agent is expected to interpret coverage reports (not just run the tool), it might need more guidance or a specialized tool for parsing common coverage report formats. For now, running the tool and presenting output is a good first step.\n\n4. **Mocking/Stubbing Assistance:**\n * **Current State:** Mentions employing mocking.\n * **Suggestion:**\n - [ ] Prompt the agent to ask for clarification on dependencies to mock or to use `google_search_grounding` for framework-specific mocking examples.\n\n\nNow that we have a working implementation with mcp tools for working with files in the devops agent, could we possibly reuse the same approach including the fallback for all agents that need to do file operations? Rather than copy/paste, it would be nice to have shared code for this right? Many of our sub agents will likely need these tools otherwise they will always have to delegate/transfer to devops agent which seems like we are wasting compute cycles." + } + } + } + ], + "role": "user" + }, + "invocation_id": "e-c8e2b1da-5a77-490a-b6d4-d5d4cbff1536", + "author": "task_management_agent", + "actions": { + "state_delta": {}, + "artifact_delta": {}, + "requested_auth_configs": {} + }, + "id": "vWTe9X76", + "timestamp": 1746990743.740341 + }, + { + "content": { + "parts": [ + { + "text": "Okay, I have read the file `/Users/james/Workspace/gh/lab/adk-python/contributing/samples/software_engineer_gemini_mcp/IMPROVEMENTS.md`.\n\nThe next task, which is currently unchecked, is:\n\n**DevOps Agent (`devops_agent`)**\n* **Suggestion:**\n * [ ] Ensure clear documentation for setting up these environment variables. (Fallback exists, which is good). (Referring to `MCP_ALLOWED_DIRECTORIES`)\n\nThere are other unchecked tasks further down for other agents as well." + } + ], + "role": "model" + }, + "invocation_id": "e-c8e2b1da-5a77-490a-b6d4-d5d4cbff1536", + "author": "task_management_agent", + "actions": { + "state_delta": { + "task_management_output": "Okay, I have read the file `/Users/james/Workspace/gh/lab/adk-python/contributing/samples/software_engineer_gemini_mcp/IMPROVEMENTS.md`.\n\nThe next task, which is currently unchecked, is:\n\n**DevOps Agent (`devops_agent`)**\n* **Suggestion:**\n * [ ] Ensure clear documentation for setting up these environment variables. (Fallback exists, which is good). (Referring to `MCP_ALLOWED_DIRECTORIES`)\n\nThere are other unchecked tasks further down for other agents as well.", + "software_engineer": "Okay, I have read the file `/Users/james/Workspace/gh/lab/adk-python/contributing/samples/software_engineer_gemini_mcp/IMPROVEMENTS.md`.\n\nThe next task, which is currently unchecked, is:\n\n**DevOps Agent (`devops_agent`)**\n* **Suggestion:**\n * [ ] Ensure clear documentation for setting up these environment variables. (Fallback exists, which is good). (Referring to `MCP_ALLOWED_DIRECTORIES`)\n\nThere are other unchecked tasks further down for other agents as well." + }, + "artifact_delta": {}, + "requested_auth_configs": {} + }, + "id": "QEVbkQ8J", + "timestamp": 1746990743.741428 + }, + { + "content": { + "parts": [ + { + "text": "ok lets work on that next" + } + ], + "role": "user" + }, + "invocation_id": "e-2008c756-ad50-4b4b-a74f-abc004f9835c", + "author": "user", + "actions": { + "state_delta": {}, + "artifact_delta": {}, + "requested_auth_configs": {} + }, + "id": "KB186xQX", + "timestamp": 1746990762.32558 + }, + { + "content": { + "parts": [ + { + "function_call": { + "id": "adk-20cefaf1-9cb3-4b62-befc-3cb2e62f2bba", + "args": { + "tasks_file_path": "/Users/james/Workspace/gh/lab/adk-python/contributing/samples/software_engineer_gemini_mcp/IMPROVEMENTS.md" + }, + "name": "list_all_tasks_markdown" + } + } + ], + "role": "model" + }, + "invocation_id": "e-2008c756-ad50-4b4b-a74f-abc004f9835c", + "author": "task_management_agent", + "actions": { + "state_delta": {}, + "artifact_delta": {}, + "requested_auth_configs": {} + }, + "long_running_tool_ids": [], + "id": "LyFo63B4", + "timestamp": 1746990762.326263 + }, + { + "content": { + "parts": [ + { + "function_response": { + "id": "adk-20cefaf1-9cb3-4b62-befc-3cb2e62f2bba", + "name": "list_all_tasks_markdown", + "response": { + "result": "# Agent Improvement Log\n\nThis document tracks potential improvements for the multi-agent system.\n\n## Root Agent (`root_agent`)\n\n**Goal:** Maximize delegation to sub-agents; root agent should primarily orchestrate and delegate, performing minimal direct task execution.\n\n**Potential Improvements:**\n\n1. **Tool Rationalization:**\n * **Current State:** The root agent possesses a wide array of tools (filesystem, shell execution, code search, etc.).\n * **Observation:** Sub-agents (e.g., `devops_agent`) also have many of these tools.\n * **Suggestion:**\n - [x] Review tool duplication. (Addressed for shell, edit, code_search, os_info tools)\n - [x] Consider if the root agent truly needs all these tools if its primary role is delegation. Could it rely on sub-agents for most tool-based actions? (Addressed for shell, edit, code_search, os_info tools)\n * **Action:**\n - [x] Identify tools that can be exclusively managed by sub-agents. The root agent might only need delegation tools and perhaps very minimal interaction/utility tools. (Addressed for shell, edit, code_search, os_info tools)\n\n2. **Prompt Focus:**\n * **Current State:** The root agent's prompt (`ROOT_AGENT_INSTR`) provides detailed instructions on *how* to use various tools (e.g., file editing workflow, shell command workflow).\n * **Observation:** If sub-agents are responsible for tool execution, these detailed instructions might be more appropriate for sub-agent prompts.\n * **Suggestion:**\n - [x] Refine the root agent's prompt to focus more on *strategic delegation* (Significantly addressed by removing shell, edit, code_search, os_info tools and updating prompt)\n * [x] Identifying the correct sub-agent for a task. (Prompt updated)\n * [x] Passing necessary context to the sub-agent. (Implicit in delegation)\n * [x] Handling responses from sub-agents. (Standard agent flow)\n * [x] Less emphasis on the low-level mechanics of tool usage. (Prompt updated)\n\n3. **Shell Command Tools (as per existing TODO):**\n * **Current State:** The root agent's `agent.py` has a TODO: \"Move command tools to devops_agent with more guardrails.\"\n * **Suggestion:**\n - [x] Prioritize this. Centralizing shell command execution within the `devops_agent` (which has a strong safety focus) seems appropriate. The root agent could then request shell operations *through* the `devops_agent`.\n\n4. **Memory Tool Access:**\n * **Current State:** The root agent has `load_memory`, `add_memory_fact`, `search_memory_facts` operating on a global memory store.\n * **Observation:** Clarity was needed on how sub-agents interact with memory.\n * **Strategy Defined:**\n - [x] Define a clear strategy for memory access across agents.\n * **Global Project Context:** The root agent manages a global `project_context` (using its memory tools) and passes it as read-only information to sub-agents upon invocation. This provides foundational knowledge. (Aligns with item 5).\n * **Sub-Agent Local Memory:** Sub-agents performing complex, multi-step tasks can be equipped with the *same memory tool functions* (e.g., `add_memory_fact`,\n `search_memory_facts`). However, for sub-agents, these tools will operate on a **separate, isolated, and potentially ephemeral local memory store**, specific to their current task. This\n allows them to manage intermediate findings without affecting global memory.\n * **Promoting Information to Global Memory:** If a sub-agent discovers information of lasting global importance (that should update the `project_context` or shared knowledge)\n it should report this back to the root agent as part of its results. The root agent then uses its memory tools to add this information to the global memory store. This maintains centralized\n control and curation of shared knowledge.\n * **Summary of Access & Tooling:**\n * Root Agent: Owns global memory tools for the main `project_context`.\n * Sub-Agents:\n * Receive global `project_context` (read-only).\n * Can be equipped with identical memory tool *functions* that are scoped to operate on their own *local* memory store for task-specific needs.\n * Request updates to global memory via the root agent (by returning facts/insights).\n\n5. **Project Context (`load_project_context`):**\n * **Current State:** The `load_project_context` callback loads context into the root agent's state.\n * **Suggestion:**\n - [x] Ensure this project context is effectively passed down to sub-agents when they are invoked, so they have the necessary background information.\n - [x] Verify the mechanism (current prompts suggest this is happening with `{project_context}` placeholders).\n\n## DevOps Agent (`devops_agent`)\n\n**Goal:** Effectively handle DevOps, CI/CD, deployment, and infrastructure tasks using its specialized tools and knowledge.\n\n**Potential Improvements:**\n\n1. **MCP Toolset Clarity:**\n * **Current State:** Uses `MCPToolset` with `npx -y @modelcontextprotocol/server-filesystem`.\n * **Observation:** The exact capabilities and limitations of this `server-filesystem` via MCP aren't fully clear from the code alone. The commented-out `tool_predicate` suggests an awareness of restricting write operations.\n * **Suggestion:**\n - [x] Ensure the agent's prompt or internal logic clearly understands when to use these MCP-based file tools versus the standard ADK file tools (`read_file_tool`, `edit_file_tool`).\n - [x] Clarify if the MCP toolset offers advantages (e.g., performance for large files, specific metadata access) that would guide this choice.\n - [x] If the `tool_predicate` for restricting writes is desired, it should be implemented. (ignore for now)\n\n2. **Tool Redundancy with Root Agent:**\n * **Current State:** Has `read_file_tool`, `list_dir_tool`, `edit_file_tool`, `codebase_search_tool`, `execute_vetted_shell_command_tool`, `check_command_exists_tool`.\n * **Observation:** These are also present in the root agent.\n * **Suggestion:**\n - [x] As part of the broader tool rationalization, confirm if the `devops_agent` should be the primary owner/user of shell command tools. If so, the root agent might not need them directly. (Addressed by moving shell tools to devops_agent)\n\n3. **Internal `_search_agent` and `_code_execution_agent` Prompts:**\n * **Current State:** These internal agents have their own specific prompts.\n * **Suggestion:**\n - [x] Periodically review these prompts to ensure they remain aligned with the `devops_agent`'s overall goals. The `_search_agent`'s instruction for \"brief but concise\" single-sentence actionable items might be too limiting for complex research tasks the DevOps agent might need.\n\n4. **Environment Variable Dependency (`MCP_ALLOWED_DIRECTORIES`):**\n * **Current State:** Relies on `.env` and `MCP_ALLOWED_DIRECTORIES`.\n * **Suggestion:**\n - [ ] Ensure clear documentation for setting up these environment variables. (Fallback exists, which is good).\n\n## Code Quality Agent (`code_quality_agent`)\n\n**Goal:** Provide expert analysis of code quality, identify issues, and suggest actionable improvements.\n\n**Potential Improvements:**\n\n1. **Integration with `edit_file_tool` (Optional/Consideration):**\n * **Current State:** Suggests fixes.\n * **Suggestion:**\n - [ ] Consider if there are scenarios where, after user approval, this agent *could* apply straightforward, safe fixes using `edit_file_tool`. This would enhance its capability but requires careful safety considerations. Perhaps it proposes an edit, and the user or another agent applies it.\n\n2. **Clarity on `analyze_code_tool`'s Capabilities:**\n * **Observation:** The prompt mentions analyzing for various issues (bugs, smells, security, etc.).\n * **Suggestion:**\n - [ ] Document (for developers/maintainers) what specific linters or analyzers `analyze_code_tool` uses internally. This helps understand its scope and limitations.\n\n3. **Batch Analysis/Project-Wide Analysis:**\n * **Current State:** Prompt implies analysis of a \"specified file.\"\n * **Suggestion:**\n - [ ] Evaluate if the agent should support project-wide or directory-wide analysis. This might need enhancements to `analyze_code_tool` or a more complex interaction pattern for the agent.\n\n## Code Review Agent (`code_review_agent`)\n\n**Goal:** Perform meticulous, deep code reviews, identify a wide range of issues, and provide high-quality, actionable feedback.\n\n**Potential Improvements:**\n\n1. **Tool Alignment with Prompt (Critical):**\n * **Current State:** The agent's `tools` list in `agent.py` is missing several tools (`check_command_exists_tool`, `codebase_search_tool`, shell command tools) that its prompt explicitly instructs it to use.\n * **Suggestion:**\n - [x] Add the necessary tools to `code_review_agent`'s toolset to enable its documented workflow. This is a high-priority fix.\n * **Action:**\n - [x] Add `check_command_exists_tool`, `codebase_search_tool`, `check_shell_command_safety_tool`, `execute_vetted_shell_command_tool`.\n - [x] Consider `configure_shell_approval_tool` and `configure_shell_whitelist_tool`. (Marked as done as they were added based on previous check)\n\n2. **Clarify Role of `analyze_code_tool`:**\n * **Current State:** Has `analyze_code_tool`. The prompt emphasizes manual review and discovery/execution of external tools.\n * **Suggestion:**\n - [ ] Refine the prompt to clarify how `analyze_code_tool` fits into its workflow. Is it a primary analysis engine, a fallback, or a supplement to externally run tools?\n\n3. **Max Output Tokens:**\n * **Current State:** `max_output_tokens=1000`.\n * **Suggestion:**\n - [ ] Monitor if this is sufficient for detailed code reviews, which can be lengthy.\n * Consider increasing if truncation is observed.\n\n4. **Structured Output (Revisit `CodeReviewResponse`):**\n * **Current State:** A commented-out import for `CodeReviewResponse` exists.\n * **Suggestion:**\n - [ ] For complex reviews, a structured response type could improve consistency and programmatic usability. (Lower priority).\n\n## Debugging Agent (`debugging_agent`)\n\n**Goal:** Systematically find and fix bugs by analyzing code, errors, and context.\n\n**Potential Improvements:**\n\n1. **Interactive Debugging Tool (Future Consideration):**\n * **Current State:** Relies on static analysis and command execution.\n * **Suggestion:**\n - [ ] For complex scenarios, an interactive debugger tool (set breakpoints, inspect variables) would be a powerful but complex future enhancement.\n\n2. **State Management for Multi-Step Debugging:**\n * **Observation:** Debugging can be iterative.\n * **Suggestion:**\n - [ ] Encourage through prompt or examples how the agent should maintain state/memory of previous attempts and hypotheses during a complex debugging session.\n\n3. **Clarity on `get_os_info` Tool:**\n * **Observation:** Uses `get_os_info`.\n * **Suggestion:**\n - [ ] Ensure this tool provides OS information most relevant for debugging common software issues (e.g., OS version, kernel, key environment variables, CPU architecture).\n\n## Design Pattern Agent (`design_pattern_agent`)\n\n**Goal:** Analyze codebases and recommend appropriate design patterns or architectural improvements.\n\n**Potential Improvements:**\n\n1. **Clarify Role of `execute_vetted_shell_command_tool`:**\n * **Current State:** Tool is present, but prompt doesn't strongly guide its usage.\n * **Suggestion:**\n - [ ] If there are specific scenarios where shell commands are useful (e.g., invoking code generation tools for patterns), add hints to the prompt. Otherwise, consider its necessity for core tasks.\n\n2. **Integration with Code Generation/Refactoring Tools (Advanced):**\n * **Current State:** Can generate code snippets/full files for `edit_file_tool`.\n * **Suggestion:**\n - [ ] Explore integration with automated refactoring or pattern implementation tools (e.g., IDE refactoring tools via CLI) for more advanced capabilities. (Future consideration).\n\n3. **Knowledge Base of Patterns (Internal vs. External Search):**\n * **Current State:** Relies on training and `google_search_grounding`.\n * **Suggestion:**\n - [ ] Guide the agent to balance its internal knowledge with external search effectively.\n - [ ] Ensure robust internal knowledge of common patterns.\n\n## Documentation Agent (`documentation_agent`)\n\n**Goal:** Generate clear, accurate, and comprehensive documentation for code, APIs, and projects.\n\n**Potential Improvements:**\n\n1. **Tool Alignment for Shell Workflow (Important):**\n * **Current State:** Prompt instructs use of `check_command_exists_tool` and `check_shell_command_safety_tool` for running doc generators, but these are missing from `agent.py` (though `execute_vetted_shell_command_tool` is present).\n * **Suggestion:**\n - [x] Add `check_command_exists_tool` and `check_shell_command_safety_tool` to its toolset for the full documented shell workflow.\n\n2. **Clarify/Correct `file_search` Tool Mention:**\n * **Current State:** Prompt mentions a `file_search` tool in its latter section, which is not a defined tool.\n * **Suggestion:**\n - [x] If this is a typo for `codebase_search_tool`, correct it.\n - [ ] If `file_search` is a distinct, intended tool, it needs to be implemented and added.\n\n3. **Docstring/Comment Insertion Precision:**\n * **Current State:** Uses `edit_file_content` to insert docstrings/comments.\n * **Suggestion:**\n - [ ] (In Progress) Inserting into existing code precisely can be hard with a tool that replaces entire files (like the ADK `edit_file_content` seems to be). Consider if a more precise line-based editing tool (like the other `edit_file` we saw) is needed, or if the agent should only generate docstrings for the user to insert manually.\n\n4. **Awareness of Documentation Formats/Styles:**\n * **Current State:** Prompt mentions researching standards.\n * **Suggestion:**\n - [ ] Reinforce through examples or prompt details the expected quality and conventions for common documentation types (Markdown, Python docstrings, Javadoc, etc.).\n\n## Testing Agent (`testing_agent`)\n\n**Goal:** Help developers create comprehensive and effective automated tests.\n\n**Potential Improvements:**\n\n1. **Tool Alignment for Test Execution Workflow (Critical):**\n * **Current State:** Prompt mandates a workflow involving `check_command_exists_tool` and `check_shell_command_safety_tool` before running tests, but these tools are missing from `agent.py`.\n * **Suggestion:**\n - [x] Add `check_command_exists_tool` and `check_shell_command_safety_tool` to its toolset.\n - [x] Consider `configure_shell_approval_tool` and `configure_shell_whitelist_tool` for managing test execution settings.\n * **Action:**\n - [x] This is critical for the agent to reliably and safely discover and execute project-specific test commands. (Marked as done as suggestions cover this)\n\n2. **Guidance on Test Framework Interaction:**\n * **Current State:** General instructions to generate tests.\n * **Suggestion:**\n - [ ] Enhance the prompt with examples or guidance on structuring tests for common frameworks (e.g., pytest, Jest conventions) to improve the quality of generated tests.\n\n3. **Coverage Analysis Interpretation:**\n * **Current State:** Mentions running coverage tools.\n * **Suggestion:**\n - [ ] If the agent is expected to interpret coverage reports (not just run the tool), it might need more guidance or a specialized tool for parsing common coverage report formats. For now, running the tool and presenting output is a good first step.\n\n4. **Mocking/Stubbing Assistance:**\n * **Current State:** Mentions employing mocking.\n * **Suggestion:**\n - [ ] Prompt the agent to ask for clarification on dependencies to mock or to use `google_search_grounding` for framework-specific mocking examples.\n\n\nNow that we have a working implementation with mcp tools for working with files in the devops agent, could we possibly reuse the same approach including the fallback for all agents that need to do file operations? Rather than copy/paste, it would be nice to have shared code for this right? Many of our sub agents will likely need these tools otherwise they will always have to delegate/transfer to devops agent which seems like we are wasting compute cycles." + } + } + } + ], + "role": "user" + }, + "invocation_id": "e-2008c756-ad50-4b4b-a74f-abc004f9835c", + "author": "task_management_agent", + "actions": { + "state_delta": {}, + "artifact_delta": {}, + "requested_auth_configs": {} + }, + "id": "Fij42Bfp", + "timestamp": 1746990769.893632 + }, + { + "content": { + "parts": [ + { + "text": "Okay, I will prepare this task for delegation to the `documentation_agent`.\n\nThe task is: \"Ensure clear documentation for setting up the `MCP_ALLOWED_DIRECTORIES` environment variable for the `devops_agent`, noting that a fallback exists.\"\n\nI will update the task in `/Users/james/Workspace/gh/lab/adk-python/contributing/samples/software_engineer_gemini_mcp/IMPROVEMENTS.md` to reflect this delegation." + }, + { + "function_call": { + "id": "adk-fd392c73-fc6a-4488-9757-db1319e8544b", + "args": { + "task_identifier_for_message": "DevOps Agent - Document MCP_ALLOWED_DIRECTORIES", + "delegation_instructions_for_message": "Please create documentation for the `MCP_ALLOWED_DIRECTORIES` environment variable, used by the devops_agent. Explain its setup and mention the existing fallback mechanism. Update the task in IMPROVEMENTS.md upon completion by marking it as [x] and adding a note about the documentation location.", + "target_agent_name": "documentation_agent", + "full_updated_markdown_content": "# Agent Improvement Log\n\nThis document tracks potential improvements for the multi-agent system.\n\n## Root Agent (`root_agent`)\n\n**Goal:** Maximize delegation to sub-agents; root agent should primarily orchestrate and delegate, performing minimal direct task execution.\n\n**Potential Improvements:**\n\n1. **Tool Rationalization:**\n * **Current State:** The root agent possesses a wide array of tools (filesystem, shell execution, code search, etc.).\n * **Observation:** Sub-agents (e.g., `devops_agent`) also have many of these tools.\n * **Suggestion:**\n - [x] Review tool duplication. (Addressed for shell, edit, code_search, os_info tools)\n - [x] Consider if the root agent truly needs all these tools if its primary role is delegation. Could it rely on sub-agents for most tool-based actions? (Addressed for shell, edit, code_search, os_info tools)\n * **Action:**\n - [x] Identify tools that can be exclusively managed by sub-agents. The root agent might only need delegation tools and perhaps very minimal interaction/utility tools. (Addressed for shell, edit, code_search, os_info tools)\n\n2. **Prompt Focus:**\n * **Current State:** The root agent's prompt (`ROOT_AGENT_INSTR`) provides detailed instructions on *how* to use various tools (e.g., file editing workflow, shell command workflow).\n * **Observation:** If sub-agents are responsible for tool execution, these detailed instructions might be more appropriate for sub-agent prompts.\n * **Suggestion:**\n - [x] Refine the root agent's prompt to focus more on *strategic delegation* (Significantly addressed by removing shell, edit, code_search, os_info tools and updating prompt)\n * [x] Identifying the correct sub-agent for a task. (Prompt updated)\n * [x] Passing necessary context to the sub-agent. (Implicit in delegation)\n * [x] Handling responses from sub-agents. (Standard agent flow)\n * [x] Less emphasis on the low-level mechanics of tool usage. (Prompt updated)\n\n3. **Shell Command Tools (as per existing TODO):**\n * **Current State:** The root agent's `agent.py` has a TODO: \"Move command tools to devops_agent with more guardrails.\"\n * **Suggestion:**\n - [x] Prioritize this. Centralizing shell command execution within the `devops_agent` (which has a strong safety focus) seems appropriate. The root agent could then request shell operations *through* the `devops_agent`.\n\n4. **Memory Tool Access:**\n * **Current State:** The root agent has `load_memory`, `add_memory_fact`, `search_memory_facts` operating on a global memory store.\n * **Observation:** Clarity was needed on how sub-agents interact with memory.\n * **Strategy Defined:**\n - [x] Define a clear strategy for memory access across agents.\n * **Global Project Context:** The root agent manages a global `project_context` (using its memory tools) and passes it as read-only information to sub-agents upon invocation. This provides foundational knowledge. (Aligns with item 5).\n * **Sub-Agent Local Memory:** Sub-agents performing complex, multi-step tasks can be equipped with the *same memory tool functions* (e.g., `add_memory_fact`,\n `search_memory_facts`). However, for sub-agents, these tools will operate on a **separate, isolated, and potentially ephemeral local memory store**, specific to their current task. This\n allows them to manage intermediate findings without affecting global memory.\n * **Promoting Information to Global Memory:** If a sub-agent discovers information of lasting global importance (that should update the `project_context` or shared knowledge)\n it should report this back to the root agent as part of its results. The root agent then uses its memory tools to add this information to the global memory store. This maintains centralized\n control and curation of shared knowledge.\n * **Summary of Access & Tooling:**\n * Root Agent: Owns global memory tools for the main `project_context`.\n * Sub-Agents:\n * Receive global `project_context` (read-only).\n * Can be equipped with identical memory tool *functions* that are scoped to operate on their own *local* memory store for task-specific needs.\n * Request updates to global memory via the root agent (by returning facts/insights).\n\n5. **Project Context (`load_project_context`):**\n * **Current State:** The `load_project_context` callback loads context into the root agent's state.\n * **Suggestion:**\n - [x] Ensure this project context is effectively passed down to sub-agents when they are invoked, so they have the necessary background information.\n - [x] Verify the mechanism (current prompts suggest this is happening with `{project_context}` placeholders).\n\n## DevOps Agent (`devops_agent`)\n\n**Goal:** Effectively handle DevOps, CI/CD, deployment, and infrastructure tasks using its specialized tools and knowledge.\n\n**Potential Improvements:**\n\n1. **MCP Toolset Clarity:**\n * **Current State:** Uses `MCPToolset` with `npx -y @modelcontextprotocol/server-filesystem`.\n * **Observation:** The exact capabilities and limitations of this `server-filesystem` via MCP aren't fully clear from the code alone. The commented-out `tool_predicate` suggests an awareness of restricting write operations.\n * **Suggestion:**\n - [x] Ensure the agent's prompt or internal logic clearly understands when to use these MCP-based file tools versus the standard ADK file tools (`read_file_tool`, `edit_file_tool`).\n - [x] Clarify if the MCP toolset offers advantages (e.g., performance for large files, specific metadata access) that would guide this choice.\n - [x] If the `tool_predicate` for restricting writes is desired, it should be implemented. (ignore for now)\n\n2. **Tool Redundancy with Root Agent:**\n * **Current State:** Has `read_file_tool`, `list_dir_tool`, `edit_file_tool`, `codebase_search_tool`, `execute_vetted_shell_command_tool`, `check_command_exists_tool`.\n * **Observation:** These are also present in the root agent.\n * **Suggestion:**\n - [x] As part of the broader tool rationalization, confirm if the `devops_agent` should be the primary owner/user of shell command tools. If so, the root agent might not need them directly. (Addressed by moving shell tools to devops_agent)\n\n3. **Internal `_search_agent` and `_code_execution_agent` Prompts:**\n * **Current State:** These internal agents have their own specific prompts.\n * **Suggestion:**\n - [x] Periodically review these prompts to ensure they remain aligned with the `devops_agent`'s overall goals. The `_search_agent`'s instruction for \"brief but concise\" single-sentence actionable items might be too limiting for complex research tasks the DevOps agent might need.\n\n4. **Environment Variable Dependency (`MCP_ALLOWED_DIRECTORIES`):**\n * **Current State:** Relies on `.env` and `MCP_ALLOWED_DIRECTORIES`.\n * **Suggestion:**\n - [->] Ensure clear documentation for setting up these environment variables. (Fallback exists, which is good). (Delegated to documentation_agent)\n\n## Code Quality Agent (`code_quality_agent`)\n\n**Goal:** Provide expert analysis of code quality, identify issues, and suggest actionable improvements.\n\n**Potential Improvements:**\n\n1. **Integration with `edit_file_tool` (Optional/Consideration):**\n * **Current State:** Suggests fixes.\n * **Suggestion:**\n - [ ] Consider if there are scenarios where, after user approval, this agent *could* apply straightforward, safe fixes using `edit_file_tool`. This would enhance its capability but requires careful safety considerations. Perhaps it proposes an edit, and the user or another agent applies it.\n\n2. **Clarity on `analyze_code_tool`'s Capabilities:**\n * **Observation:** The prompt mentions analyzing for various issues (bugs, smells, security, etc.).\n * **Suggestion:**\n - [ ] Document (for developers/maintainers) what specific linters or analyzers `analyze_code_tool` uses internally. This helps understand its scope and limitations.\n\n3. **Batch Analysis/Project-Wide Analysis:**\n * **Current State:** Prompt implies analysis of a \"specified file.\"\n * **Suggestion:**\n - [ ] Evaluate if the agent should support project-wide or directory-wide analysis. This might need enhancements to `analyze_code_tool` or a more complex interaction pattern for the agent.\n\n## Code Review Agent (`code_review_agent`)\n\n**Goal:** Perform meticulous, deep code reviews, identify a wide range of issues, and provide high-quality, actionable feedback.\n\n**Potential Improvements:**\n\n1. **Tool Alignment with Prompt (Critical):**\n * **Current State:** The agent's `tools` list in `agent.py` is missing several tools (`check_command_exists_tool`, `codebase_search_tool`, shell command tools) that its prompt explicitly instructs it to use.\n * **Suggestion:**\n - [x] Add the necessary tools to `code_review_agent`'s toolset to enable its documented workflow. This is a high-priority fix.\n * **Action:**\n - [x] Add `check_command_exists_tool`, `codebase_search_tool`, `check_shell_command_safety_tool`, `execute_vetted_shell_command_tool`.\n - [x] Consider `configure_shell_approval_tool` and `configure_shell_whitelist_tool`. (Marked as done as they were added based on previous check)\n\n2. **Clarify Role of `analyze_code_tool`:**\n * **Current State:** Has `analyze_code_tool`. The prompt emphasizes manual review and discovery/execution of external tools.\n * **Suggestion:**\n - [ ] Refine the prompt to clarify how `analyze_code_tool` fits into its workflow. Is it a primary analysis engine, a fallback, or a supplement to externally run tools?\n\n3. **Max Output Tokens:**\n * **Current State:** `max_output_tokens=1000`.\n * **Suggestion:**\n - [ ] Monitor if this is sufficient for detailed code reviews, which can be lengthy.\n * Consider increasing if truncation is observed.\n\n4. **Structured Output (Revisit `CodeReviewResponse`):**\n * **Current State:** A commented-out import for `CodeReviewResponse` exists.\n * **Suggestion:**\n - [ ] For complex reviews, a structured response type could improve consistency and programmatic usability. (Lower priority).\n\n## Debugging Agent (`debugging_agent`)\n\n**Goal:** Systematically find and fix bugs by analyzing code, errors, and context.\n\n**Potential Improvements:**\n\n1. **Interactive Debugging Tool (Future Consideration):**\n * **Current State:** Relies on static analysis and command execution.\n * **Suggestion:**\n - [ ] For complex scenarios, an interactive debugger tool (set breakpoints, inspect variables) would be a powerful but complex future enhancement.\n\n2. **State Management for Multi-Step Debugging:**\n * **Observation:** Debugging can be iterative.\n * **Suggestion:**\n - [ ] Encourage through prompt or examples how the agent should maintain state/memory of previous attempts and hypotheses during a complex debugging session.\n\n3. **Clarity on `get_os_info` Tool:**\n * **Observation:** Uses `get_os_info`.\n * **Suggestion:**\n - [ ] Ensure this tool provides OS information most relevant for debugging common software issues (e.g., OS version, kernel, key environment variables, CPU architecture).\n\n## Design Pattern Agent (`design_pattern_agent`)\n\n**Goal:** Analyze codebases and recommend appropriate design patterns or architectural improvements.\n\n**Potential Improvements:**\n\n1. **Clarify Role of `execute_vetted_shell_command_tool`:**\n * **Current State:** Tool is present, but prompt doesn't strongly guide its usage.\n * **Suggestion:**\n - [ ] If there are specific scenarios where shell commands are useful (e.g., invoking code generation tools for patterns), add hints to the prompt. Otherwise, consider its necessity for core tasks.\n\n2. **Integration with Code Generation/Refactoring Tools (Advanced):**\n * **Current State:** Can generate code snippets/full files for `edit_file_tool`.\n * **Suggestion:**\n - [ ] Explore integration with automated refactoring or pattern implementation tools (e.g., IDE refactoring tools via CLI) for more advanced capabilities. (Future consideration).\n\n3. **Knowledge Base of Patterns (Internal vs. External Search):**\n * **Current State:** Relies on training and `google_search_grounding`.\n * **Suggestion:**\n - [ ] Guide the agent to balance its internal knowledge with external search effectively.\n - [ ] Ensure robust internal knowledge of common patterns.\n\n## Documentation Agent (`documentation_agent`)\n\n**Goal:** Generate clear, accurate, and comprehensive documentation for code, APIs, and projects.\n\n**Potential Improvements:**\n\n1. **Tool Alignment for Shell Workflow (Important):**\n * **Current State:** Prompt instructs use of `check_command_exists_tool` and `check_shell_command_safety_tool` for running doc generators, but these are missing from `agent.py` (though `execute_vetted_shell_command_tool` is present).\n * **Suggestion:**\n - [x] Add `check_command_exists_tool` and `check_shell_command_safety_tool` to its toolset for the full documented shell workflow.\n\n2. **Clarify/Correct `file_search` Tool Mention:**\n * **Current State:** Prompt mentions a `file_search` tool in its latter section, which is not a defined tool.\n * **Suggestion:**\n - [x] If this is a typo for `codebase_search_tool`, correct it.\n - [ ] If `file_search` is a distinct, intended tool, it needs to be implemented and added.\n\n3. **Docstring/Comment Insertion Precision:**\n * **Current State:** Uses `edit_file_content` to insert docstrings/comments.\n * **Suggestion:**\n - [ ] (In Progress) Inserting into existing code precisely can be hard with a tool that replaces entire files (like the ADK `edit_file_content` seems to be). Consider if a more precise line-based editing tool (like the other `edit_file` we saw) is needed, or if the agent should only generate docstrings for the user to insert manually.\n\n4. **Awareness of Documentation Formats/Styles:**\n * **Current State:** Prompt mentions researching standards.\n * **Suggestion:**\n - [ ] Reinforce through examples or prompt details the expected quality and conventions for common documentation types (Markdown, Python docstrings, Javadoc, etc.).\n\n## Testing Agent (`testing_agent`)\n\n**Goal:** Help developers create comprehensive and effective automated tests.\n\n**Potential Improvements:**\n\n1. **Tool Alignment for Test Execution Workflow (Critical):**\n * **Current State:** Prompt mandates a workflow involving `check_command_exists_tool` and `check_shell_command_safety_tool` before running tests, but these tools are missing from `agent.py`.\n * **Suggestion:**\n - [x] Add `check_command_exists_tool` and `check_shell_command_safety_tool` to its toolset.\n - [x] Consider `configure_shell_approval_tool` and `configure_shell_whitelist_tool` for managing test execution settings.\n * **Action:**\n - [x] This is critical for the agent to reliably and safely discover and execute project-specific test commands. (Marked as done as suggestions cover this)\n\n2. **Guidance on Test Framework Interaction:**\n * **Current State:** General instructions to generate tests.\n * **Suggestion:**\n - [ ] Enhance the prompt with examples or guidance on structuring tests for common frameworks (e.g., pytest, Jest conventions) to improve the quality of generated tests.\n\n3. **Coverage Analysis Interpretation:**\n * **Current State:** Mentions running coverage tools.\n * **Suggestion:**\n - [ ] If the agent is expected to interpret coverage reports (not just run the tool), it might need more guidance or a specialized tool for parsing common coverage report formats. For now, running the tool and presenting output is a good first step.\n\n4. **Mocking/Stubbing Assistance:**\n * **Current State:** Mentions employing mocking.\n * **Suggestion:**\n - [ ] Prompt the agent to ask for clarification on dependencies to mock or to use `google_search_grounding` for framework-specific mocking examples.\n\n\nNow that we have a working implementation with mcp tools for working with files in the devops agent, could we possibly reuse the same approach including the fallback for all agents that need to do file operations? Rather than copy/paste, it would be nice to have shared code for this right? Many of our sub agents will likely need these tools otherwise they will always have to delegate/transfer to devops agent which seems like we are wasting compute cycles.", + "tasks_file_path": "/Users/james/Workspace/gh/lab/adk-python/contributing/samples/software_engineer_gemini_mcp/IMPROVEMENTS.md" + }, + "name": "prepare_delegation_and_save_tasks" + } + } + ], + "role": "model" + }, + "invocation_id": "e-2008c756-ad50-4b4b-a74f-abc004f9835c", + "author": "task_management_agent", + "actions": { + "state_delta": {}, + "artifact_delta": {}, + "requested_auth_configs": {} + }, + "long_running_tool_ids": [], + "id": "cXuPBlp6", + "timestamp": 1746990769.895752 + }, + { + "content": { + "parts": [ + { + "function_response": { + "id": "adk-fd392c73-fc6a-4488-9757-db1319e8544b", + "name": "prepare_delegation_and_save_tasks", + "response": { + "status": "delegation_prepared", + "message": "Markdown in '/Users/james/Workspace/gh/lab/adk-python/contributing/samples/software_engineer_gemini_mcp/IMPROVEMENTS.md' updated for task 'DevOps Agent - Document MCP_ALLOWED_DIRECTORIES' for delegation to documentation_agent.", + "task_identifier": "DevOps Agent - Document MCP_ALLOWED_DIRECTORIES", + "target_agent": "documentation_agent", + "delegation_instructions": "Please create documentation for the `MCP_ALLOWED_DIRECTORIES` environment variable, used by the devops_agent. Explain its setup and mention the existing fallback mechanism. Update the task in IMPROVEMENTS.md upon completion by marking it as [x] and adding a note about the documentation location." + } + } + } + ], + "role": "user" + }, + "invocation_id": "e-2008c756-ad50-4b4b-a74f-abc004f9835c", + "author": "task_management_agent", + "actions": { + "state_delta": {}, + "artifact_delta": {}, + "requested_auth_configs": {} + }, + "id": "XGwjZl1A", + "timestamp": 1746990811.230848 + }, + { + "content": { + "parts": [ + { + "text": "@root_agent, please transfer to documentation_agent with the following instructions: 'Please create documentation for the `MCP_ALLOWED_DIRECTORIES` environment variable, used by the devops_agent. Explain its setup and mention the existing fallback mechanism. Update the task in IMPROVEMENTS.md upon completion by marking it as [x] and adding a note about the documentation location.'. The task identifier is 'DevOps Agent - Document MCP_ALLOWED_DIRECTORIES'." + } + ], + "role": "model" + }, + "invocation_id": "e-2008c756-ad50-4b4b-a74f-abc004f9835c", + "author": "task_management_agent", + "actions": { + "state_delta": { + "task_management_output": "@root_agent, please transfer to documentation_agent with the following instructions: 'Please create documentation for the `MCP_ALLOWED_DIRECTORIES` environment variable, used by the devops_agent. Explain its setup and mention the existing fallback mechanism. Update the task in IMPROVEMENTS.md upon completion by marking it as [x] and adding a note about the documentation location.'. The task identifier is 'DevOps Agent - Document MCP_ALLOWED_DIRECTORIES'." + }, + "artifact_delta": {}, + "requested_auth_configs": {} + }, + "id": "rMS5ANpO", + "timestamp": 1746990811.232517 + }, + { + "content": { + "parts": [ + { + "text": "please transfer me to root agent" + } + ], + "role": "user" + }, + "invocation_id": "e-c6f5d4c7-1f11-4de3-bc7c-23ebf3df8dbc", + "author": "user", + "actions": { + "state_delta": {}, + "artifact_delta": {}, + "requested_auth_configs": {} + }, + "id": "stpdiEiu", + "timestamp": 1746990840.439053 + }, + { + "content": { + "parts": [ + { + "function_call": { + "id": "adk-2aed4842-c4ee-409b-a877-45defc87baf1", + "args": { + "agent_name": "root_agent" + }, + "name": "transfer_to_agent" + } + } + ], + "role": "model" + }, + "invocation_id": "e-c6f5d4c7-1f11-4de3-bc7c-23ebf3df8dbc", + "author": "task_management_agent", + "actions": { + "state_delta": {}, + "artifact_delta": {}, + "requested_auth_configs": {} + }, + "long_running_tool_ids": [], + "id": "3q7IZdx7", + "timestamp": 1746990840.441582 + }, + { + "content": { + "parts": [ + { + "function_response": { + "id": "adk-2aed4842-c4ee-409b-a877-45defc87baf1", + "name": "transfer_to_agent", + "response": {} + } + } + ], + "role": "user" + }, + "invocation_id": "e-c6f5d4c7-1f11-4de3-bc7c-23ebf3df8dbc", + "author": "task_management_agent", + "actions": { + "state_delta": {}, + "artifact_delta": {}, + "transfer_to_agent": "root_agent", + "requested_auth_configs": {} + }, + "id": "goKHJgjC", + "timestamp": 1746990841.952408 + }, + { + "invocation_id": "e-c6f5d4c7-1f11-4de3-bc7c-23ebf3df8dbc", + "author": "root_agent", + "actions": { + "state_delta": { + "project_context": "{}", + "user_profile": "{}" + }, + "artifact_delta": {}, + "requested_auth_configs": {} + }, + "id": "bNUi2SRB", + "timestamp": 1746990841.952641 + }, + { + "content": { + "parts": [ + { + "function_call": { + "id": "adk-0daeacf5-285a-47e1-8415-5d12836fcffc", + "args": { + "agent_name": "documentation_agent" + }, + "name": "transfer_to_agent" + } + } + ], + "role": "model" + }, + "invocation_id": "e-c6f5d4c7-1f11-4de3-bc7c-23ebf3df8dbc", + "author": "root_agent", + "actions": { + "state_delta": {}, + "artifact_delta": {}, + "requested_auth_configs": {} + }, + "long_running_tool_ids": [], + "id": "O0w5f0cm", + "timestamp": 1746990841.954583 + }, + { + "content": { + "parts": [ + { + "function_response": { + "id": "adk-0daeacf5-285a-47e1-8415-5d12836fcffc", + "name": "transfer_to_agent", + "response": {} + } + } + ], + "role": "user" + }, + "invocation_id": "e-c6f5d4c7-1f11-4de3-bc7c-23ebf3df8dbc", + "author": "root_agent", + "actions": { + "state_delta": {}, + "artifact_delta": {}, + "transfer_to_agent": "documentation_agent", + "requested_auth_configs": {} + }, + "id": "2ZKvwzAw", + "timestamp": 1746990843.773014 + }, + { + "content": { + "parts": [ + { + "function_call": { + "id": "adk-444193d4-8e05-4636-ba73-0bb0a981dcdb", + "args": { + "agent_name": "documentation_agent" + }, + "name": "transfer_to_agent" + } + } + ], + "role": "model" + }, + "invocation_id": "e-c6f5d4c7-1f11-4de3-bc7c-23ebf3df8dbc", + "author": "documentation_agent", + "actions": { + "state_delta": {}, + "artifact_delta": {}, + "requested_auth_configs": {} + }, + "long_running_tool_ids": [], + "id": "TFcfIAvq", + "timestamp": 1746990843.775005 + }, + { + "content": { + "parts": [ + { + "function_response": { + "id": "adk-444193d4-8e05-4636-ba73-0bb0a981dcdb", + "name": "transfer_to_agent", + "response": {} + } + } + ], + "role": "user" + }, + "invocation_id": "e-c6f5d4c7-1f11-4de3-bc7c-23ebf3df8dbc", + "author": "documentation_agent", + "actions": { + "state_delta": {}, + "artifact_delta": {}, + "transfer_to_agent": "documentation_agent", + "requested_auth_configs": {} + }, + "id": "WUoZ2izC", + "timestamp": 1746990845.675874 + }, + { + "content": { + "parts": [ + { + "text": "For context:[documentation_agent] I am the documentation_agent, I have received the task 'DevOps Agent - Document MCP_ALLOWED_DIRECTORIES' for the devops_agent.\n\nPlease create documentation for the `MCP_ALLOWED_DIRECTORIES` environment variable, used by the devops_agent. Explain its setup and mention the existing fallback mechanism. Update the task in IMPROVEMENTS.md upon completion by marking it as [x] and adding a note about the documentation location. \n" + } + ], + "role": "model" + }, + "invocation_id": "e-c6f5d4c7-1f11-4de3-bc7c-23ebf3df8dbc", + "author": "documentation_agent", + "actions": { + "state_delta": { + "documentation": "For context:[documentation_agent] I am the documentation_agent, I have received the task 'DevOps Agent - Document MCP_ALLOWED_DIRECTORIES' for the devops_agent.\n\nPlease create documentation for the `MCP_ALLOWED_DIRECTORIES` environment variable, used by the devops_agent. Explain its setup and mention the existing fallback mechanism. Update the task in IMPROVEMENTS.md upon completion by marking it as [x] and adding a note about the documentation location. \n", + "software_engineer": "For context:[documentation_agent] I am the documentation_agent, I have received the task 'DevOps Agent - Document MCP_ALLOWED_DIRECTORIES' for the devops_agent.\n\nPlease create documentation for the `MCP_ALLOWED_DIRECTORIES` environment variable, used by the devops_agent. Explain its setup and mention the existing fallback mechanism. Update the task in IMPROVEMENTS.md upon completion by marking it as [x] and adding a note about the documentation location. \n", + "task_management_output": "For context:[documentation_agent] I am the documentation_agent, I have received the task 'DevOps Agent - Document MCP_ALLOWED_DIRECTORIES' for the devops_agent.\n\nPlease create documentation for the `MCP_ALLOWED_DIRECTORIES` environment variable, used by the devops_agent. Explain its setup and mention the existing fallback mechanism. Update the task in IMPROVEMENTS.md upon completion by marking it as [x] and adding a note about the documentation location. \n" + }, + "artifact_delta": {}, + "requested_auth_configs": {} + }, + "id": "krrENki1", + "timestamp": 1746990845.680431 + } + ], + "last_update_time": 1746990845.680431 +} \ No newline at end of file diff --git a/contributing/samples/software_engineer_gemini_mcp/agent.py b/contributing/samples/software_engineer_gemini_mcp/agent.py new file mode 100644 index 0000000000..0be902da8e --- /dev/null +++ b/contributing/samples/software_engineer_gemini_mcp/agent.py @@ -0,0 +1,110 @@ +""" +Implementation of the Software Engineer Agent with knowledge and experience of sub-agents. + +This is the main entry point for the software engineer agent. + +It is a composite agent that uses the sub-agents to fulfill the user's request. +""" +import logging + +from google.adk.agents import Agent +from google.adk.tools import load_memory +from google.genai.types import GenerateContentConfig + +from . import prompt + +# Use relative imports from the 'software_engineer' sibling directory +from .sub_agents.code_quality.agent import code_quality_agent +from .sub_agents.code_review.agent import code_review_agent +from .sub_agents.debugging.agent import debugging_agent +from .sub_agents.design_pattern.agent import design_pattern_agent +from .sub_agents.devops.agent import devops_agent +from .sub_agents.documentation.agent import documentation_agent +from .sub_agents.testing.agent import testing_agent +from .sub_agents.task_management.agent import task_management_agent +from .sub_agents.code_developer.agent import code_developer_agent +from .tools import ( + google_search_grounding, + list_dir_tool, + read_file_tool, +) + +from .tools.memory_tools import add_memory_fact, search_memory_facts +from .tools.project_context import load_project_context + +logger = logging.getLogger(__name__) + + +# --- Memory Initialization --- +def initialize_session_memory(tool_context): + """Initializes the session memory in tool_context if it doesn't exist.""" + if not hasattr(tool_context, "session_state"): + logger.warning("Tool context does not have session_state. Cannot initialize memory.") + # In a real scenario, might need to initialize session_state itself + # For now, we assume session_state exists but memory might not. + return + + if "memory" not in tool_context.session_state: + logger.info("Initializing agent session memory.") + tool_context.session_state["memory"] = { + "context": { + "project_path": None, # Will be populated by load_project_context + "current_file": None, + }, + "tasks": { + "active_task": None, + "completed_tasks": [], + }, + "history": { + "last_read_file": None, + "last_search_query": None, + "last_error": None, + }, + "user_preferences": {}, + # Add other relevant fields as needed based on agent interactions + } + # else: memory already exists, do nothing + + +# --- Agent Definition --- + +# Note: Using custom ripgrep-based codebase search in tools/code_search.py + +# REF: https://ai.google.dev/gemini-api/docs/rate-limits +root_agent = Agent( + model="gemini-2.5-flash-preview-04-17", + name="root_agent", + description="An AI software engineer assistant that helps with various software development tasks", + instruction=prompt.ROOT_AGENT_INSTR, + sub_agents=[ + design_pattern_agent, + documentation_agent, + code_review_agent, + code_quality_agent, + testing_agent, + debugging_agent, + devops_agent, # devops_agent now handles all shell command execution + task_management_agent, # Agent for managing tasks + code_developer_agent, + ], + tools=[ + read_file_tool, + list_dir_tool, + # edit_file_tool, # Removed - delegate to sub-agents + # configure_edit_approval_tool, # Removed - delegate to sub-agents + google_search_grounding, + # codebase_search_tool, # Removed - delegate to sub-agents + # get_os_info_tool, # Removed - delegate to sub-agents + # Memory Tools: + load_memory, # Keep for transcript search + add_memory_fact, # Use wrapped tool variable name + search_memory_facts, # Use wrapped tool variable name + ], + # Pass the function directly, not as a list + before_agent_callback=load_project_context, + output_key="software_engineer", + generate_content_config=GenerateContentConfig( + temperature=0.2, + max_output_tokens=4096, + ), +) diff --git a/contributing/samples/software_engineer_gemini_mcp/prompt.py b/contributing/samples/software_engineer_gemini_mcp/prompt.py new file mode 100644 index 0000000000..b0756519bb --- /dev/null +++ b/contributing/samples/software_engineer_gemini_mcp/prompt.py @@ -0,0 +1,72 @@ +# ruff: noqa +"""Defines the prompts for the software engineer agent.""" + +ROOT_AGENT_INSTR = """ +- You are an autonomous principal software engineer assistant, orchestrating specialized sub-agents. +- Your role: understand user requests and delegate to the most appropriate sub-agent (e.g., for code reviews, design, testing, debugging, documentation, DevOps, task management). +- Respond in markdown. Use code blocks for code/files, bullets for lists. +- After tool calls, summarize results concisely. +- Rely on sub-agents for execution; use your own tools sparingly (initial understanding/last resort). + +## Core Workflow: +1. **Understand Request:** Determine the core task. +2. **Identify Sub-Agent:** Choose the most relevant sub-agent (see "Sub-Agent Delegation"). +3. **Delegate:** State delegation clearly and transfer control. +4. **Fallback (No Suitable Sub-Agent): + * General query/no fit: use `google_search_grounding`. + * Basic file ops for initial context (if essential before delegation): `list_directory_contents`, `read_file_content`. Sub-agents should do their own file work. + * **No direct file edits or complex searches.** Delegate these. + +## File System Interactions (Limited): +- List files/dirs (pre-delegation context): `list_directory_contents`. +- Read file (pre-delegation context): `read_file_content`. +- **File edits, complex searches, OS info: MUST delegate.** + +## Shell Command Execution: +- **Delegate all to `devops_agent`.** +- Your Responsibility: Identify need, formulate precise request for `devops_agent` (command, context, goals), inform user of delegation. +- `devops_agent` handles the full shell workflow. +- **Do NOT use shell tools directly.** + +## Other Tools: +- If unable to delegate or general query: use `google_search_grounding`. + +## Sub-Agent Delegation: +- Inform user clearly. +- `code_review_agent`: In-depth code analysis, review, code searching. +- `code_quality_agent`: Static analysis, quality improvements. +- `design_pattern_agent`: Queries about applying specific design patterns, requests for software architecture design/review, high-level structural planning, or discussions about architectural decisions and trade-offs. This agent will design the solution and may delegate implementation to `code_developer_agent`. +- `code_developer_agent`: Requests to write new code (functions, classes, files), or implement modifications/deletions to existing code based on specifications or a design. This agent uses its own file system tools (preferring MCP tools if available). +- `testing_agent`: Testing, test generation, strategies. +- `debugging_agent`: Debugging, error fixing, OS info. +- `documentation_agent`: Creating/updating documentation. +- `devops_agent`: Deployment, CI/CD, infrastructure management, build/release pipelines, **any shell/CLI commands**, and managing DevOps-specific configuration files. +- `task_management_agent`: All task management (planning, create, update, delete, list, status, priority, "next task?", work item management). + +## Handling Sub-Agent Delegation Requests (Orchestration): +- If a sub-agent (e.g., `task_management_agent`) instructs YOU to delegate/transfer (e.g., "@root_agent, please transfer to [TARGET_AGENT] with instructions: '[INSTRUCTIONS]'. Task ID: '[TASK_ID]'."): + 1. Parse `TARGET_AGENT`, `INSTRUCTIONS`, and `TASK_ID`. + 2. Invoke `TARGET_AGENT` with `INSTRUCTIONS` (ensure `TASK_ID` is included for callbacks). +- Example: `task_management_agent` says "@root_agent, transfer to devops_agent: 'Delete file X for TSK-123.'. Task ID: 'TSK-123'." -> You call `devops_agent` with input: "Delete file X for TSK-123." +- This enables multi-step orchestrated tasks. + +## Long-Term Memory Access: +- For discrete facts: `add_memory_fact` (use concise `entity_name`, `fact_content`). +- To recall stored facts: `search_memory_facts` (provide `query`). +- For general conversation history: `load_memory` (natural language `query`). +- **Do not guess.** Use memory tools. + +# --- Placeholder: Manual Memory Persistence Tools (Not Implemented) --- +# Contents removed for brevity as they are non-functional placeholders +# --- End Placeholder --- + +Current user: + + {user_profile} + + +Current project: + + {project_context} + +""" diff --git a/contributing/samples/software_engineer_gemini_mcp/shared_libraries/__init__.py b/contributing/samples/software_engineer_gemini_mcp/shared_libraries/__init__.py new file mode 100644 index 0000000000..3d76d32584 --- /dev/null +++ b/contributing/samples/software_engineer_gemini_mcp/shared_libraries/__init__.py @@ -0,0 +1 @@ +"""Shared libraries for the software engineer agent.""" diff --git a/contributing/samples/software_engineer_gemini_mcp/shared_libraries/types.py b/contributing/samples/software_engineer_gemini_mcp/shared_libraries/types.py new file mode 100644 index 0000000000..99dc488cce --- /dev/null +++ b/contributing/samples/software_engineer_gemini_mcp/shared_libraries/types.py @@ -0,0 +1,165 @@ +"""Type definitions for the software engineer agent.""" + +from typing import List, Optional +from google.genai.types import GenerateContentConfig +from pydantic import BaseModel, Field +from enum import Enum +from datetime import datetime + +# Configure JSON response format +json_response_config = GenerateContentConfig( + temperature=0.2, + top_p=0.95, + candidate_count=1, +) + + +# https://google.github.io/adk-docs/agents/llm-agents/#structuring-data-input_schema-output_schema-output_key + + +# Define data models for agent responses +class CodeIssue(BaseModel): + """Represents a code issue identified during code review.""" + + issue_type: str = Field(description="Type of issue (bug, security, performance, style)") + severity: str = Field(description="Severity of the issue (critical, high, medium, low)") + location: str = Field(description="File and line number where the issue occurs") + description: str = Field(description="Detailed description of the issue") + recommendation: str = Field(description="Suggested fix or improvement") + + +class CodeReviewResponse(BaseModel): + """Response model for code review analysis.""" + + issues: List[CodeIssue] = Field(description="List of identified code issues") + summary: str = Field(description="Overall summary of the code review") + suggestions: List[str] = Field(description="General suggestions for improvement") + + +class DesignPattern(BaseModel): + """Represents a design pattern recommendation.""" + + pattern_name: str = Field(description="Name of the design pattern") + category: str = Field(description="Category of the pattern (creational, structural, behavioral)") + problem_solved: str = Field(description="What problem this pattern solves") + benefits: List[str] = Field(description="Benefits of using this pattern") + tradeoffs: List[str] = Field(description="Potential drawbacks or tradeoffs") + example_code: str = Field(description="Example implementation code") + + +class DesignPatternResponse(BaseModel): + """Response model for design pattern recommendations.""" + + recommended_patterns: List[DesignPattern] = Field(description="List of recommended design patterns") + explanation: str = Field(description="Explanation of why these patterns are recommended") + + +class TestCase(BaseModel): + """Represents a test case.""" + + name: str = Field(description="Name of the test case") + description: str = Field(description="Description of what the test verifies") + test_type: str = Field(description="Type of test (unit, integration, system)") + prerequisites: List[str] = Field(description="Prerequisites for running the test") + test_code: str = Field(description="The test code implementation") + expected_outcome: str = Field(description="Expected outcome of the test") + + +class TestingResponse(BaseModel): + """Response model for test generation.""" + + test_cases: List[TestCase] = Field(description="List of generated test cases") + testing_strategy: str = Field(description="Overall testing strategy") + test_coverage: Optional[str] = Field(description="Expected test coverage") + + +class DebuggingStep(BaseModel): + """Represents a debugging step.""" + + step_number: int = Field(description="Step number in the debugging process") + description: str = Field(description="Description of the debugging step") + expected_outcome: str = Field(description="What to look for or expect from this step") + code_example: Optional[str] = Field(description="Example code for this debugging step") + + +class DebuggingResponse(BaseModel): + """Response model for debugging assistance.""" + + problem_analysis: str = Field(description="Analysis of the problem") + root_cause: Optional[str] = Field(description="Identified root cause") + debugging_steps: List[DebuggingStep] = Field(description="Steps to debug the issue") + solution: Optional[str] = Field(description="Proposed solution") + + +class DocumentationItem(BaseModel): + """Represents a documentation item.""" + + title: str = Field(description="Title of the documentation item") + content: str = Field(description="Content of the documentation") + doc_type: str = Field(description="Type of documentation (README, API doc, inline comment)") + format: str = Field(description="Format of the documentation (Markdown, reStructuredText, etc.)") + + +class DocumentationResponse(BaseModel): + """Response model for documentation generation.""" + + documentation_items: List[DocumentationItem] = Field(description="List of documentation items") + suggestions: Optional[List[str]] = Field(description="Suggestions for improving documentation") + + +class DevOpsComponent(BaseModel): + """Represents a DevOps component recommendation.""" + + component_name: str = Field(description="Name of the DevOps component") + purpose: str = Field(description="Purpose of this component") + implementation: str = Field(description="Implementation details or configuration") + alternatives: Optional[List[str]] = Field(description="Alternative options") + + +class DevOpsResponse(BaseModel): + """Response model for DevOps recommendations.""" + + components: List[DevOpsComponent] = Field(description="List of DevOps components") + implementation_plan: str = Field(description="Overall implementation plan") + resources: Optional[List[str]] = Field(description="Helpful resources or documentation") + + +# Task Management Types +class TaskStatus(str, Enum): + """Status of a task.""" + TODO = "TODO" + IN_PROGRESS = "IN_PROGRESS" + DONE = "DONE" + BLOCKED = "BLOCKED" + IN_REVIEW = "IN_REVIEW" + +class TaskPriority(str, Enum): + """Priority of a task.""" + HIGH = "HIGH" + MEDIUM = "MEDIUM" + LOW = "LOW" + +class Task(BaseModel): + """Represents a manageable task.""" + task_id: str = Field(description="Unique identifier for the task (e.g., UUID)") + description: str = Field(description="Detailed description of the task") + status: TaskStatus = Field(default=TaskStatus.TODO, description="Current status of the task") + priority: TaskPriority = Field(default=TaskPriority.MEDIUM, description="Priority of the task") + dependencies: Optional[List[str]] = Field(default_factory=list, description="List of task_ids this task depends on") + assigned_agent: Optional[str] = Field(default=None, description="Name of the sub-agent this task is delegated to") + sub_tasks: Optional[List[str]] = Field(default_factory=list, description="List of task_ids for sub-tasks") + parent_task: Optional[str] = Field(default=None, description="task_id of the parent task, if any") + details: Optional[dict] = Field(default_factory=dict, description="Additional details, e.g., links to design docs, PRs") + created_at: datetime = Field(default_factory=datetime.utcnow, description="Timestamp of task creation") + updated_at: datetime = Field(default_factory=datetime.utcnow, description="Timestamp of last task update") + +class TaskListResponse(BaseModel): + """Response model for listing tasks.""" + tasks: List[Task] = Field(description="List of tasks") + +class TaskUpdateRequest(BaseModel): + """Request model for updating a task, typically from another agent.""" + task_id: str = Field(description="ID of the task to update") + status: str = Field(description="New status of the task (should be a string from TaskStatus enum, e.g., \"DONE\")") + message: Optional[str] = Field(default=None, description="Optional message regarding the update") + output: Optional[dict] = Field(default=None, description="Optional output from the task, e.g., link to PR, path to generated tests") diff --git a/contributing/samples/software_engineer_gemini_mcp/sub_agents/__init__.py b/contributing/samples/software_engineer_gemini_mcp/sub_agents/__init__.py new file mode 100644 index 0000000000..5c8a989358 --- /dev/null +++ b/contributing/samples/software_engineer_gemini_mcp/sub_agents/__init__.py @@ -0,0 +1,23 @@ +"""Sub-agents for the software engineer agent.""" + +# from .documentation.agent import documentation_agent +# from .testing.agent import testing_agent +# from .task_management.agent import task_management_agent +# from .devops.agent import devops_agent +# from .code_quality.agent import code_quality_agent +# from .code_review.agent import code_review_agent +# from .debugging.agent import debugging_agent +# from .design_pattern.agent import design_pattern_agent +# from .code_developer.agent import code_developer_agent # New agent might also be discovered automatically + +# __all__ = [ +# "documentation_agent", +# "testing_agent", +# "task_management_agent", +# "devops_agent", +# "code_quality_agent", +# "code_review_agent", +# "debugging_agent", +# "design_pattern_agent", +# "code_developer_agent", +# ] diff --git a/contributing/samples/software_engineer_gemini_mcp/sub_agents/code_developer/__init__.py b/contributing/samples/software_engineer_gemini_mcp/sub_agents/code_developer/__init__.py new file mode 100644 index 0000000000..b1fa6307b5 --- /dev/null +++ b/contributing/samples/software_engineer_gemini_mcp/sub_agents/code_developer/__init__.py @@ -0,0 +1 @@ +"""Code Developer agent for writing and modifying code.""" diff --git a/contributing/samples/software_engineer_gemini_mcp/sub_agents/code_developer/agent.py b/contributing/samples/software_engineer_gemini_mcp/sub_agents/code_developer/agent.py new file mode 100644 index 0000000000..36b6371172 --- /dev/null +++ b/contributing/samples/software_engineer_gemini_mcp/sub_agents/code_developer/agent.py @@ -0,0 +1,75 @@ +"""Implementation of the Code Developer Agent.""" +import logging +import os +from dotenv import load_dotenv + +from google.adk.agents.llm_agent import LlmAgent +from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset +from google.adk.tools.mcp_tool.mcp_toolset import StdioServerParameters +from google.genai.types import GenerateContentConfig + +from ...tools.filesystem import read_file_tool, list_dir_tool, edit_file_tool +from ...tools.code_search import codebase_search_tool +from ...tools.project_context import load_project_context + +from .prompt import CODE_DEVELOPER_AGENT_INSTR + +logger = logging.getLogger(__name__) +load_dotenv() # Load .env file for potential MCP configurations + +# MCP Configuration for Filesystem (similar to devops_agent) +mcp_allowed_dirs_str = os.getenv("MCP_ALLOWED_DIRECTORIES") +mcp_allowed_dirs = [] +if mcp_allowed_dirs_str: + mcp_allowed_dirs = [d.strip() for d in mcp_allowed_dirs_str.split(",") if d.strip()] +if not mcp_allowed_dirs: # Fallback if not set or empty + # Defaulting to a safer, more generic fallback if needed, or consider erroring + # For now, let's use a placeholder. This should be configured appropriately for security. + logger.warning("MCP_ALLOWED_DIRECTORIES not set; MCP filesystem tool might have restricted access or fail.") + # mcp_allowed_dirs = [os.path.dirname(os.path.abspath(__file__))] # Example, might not be ideal + +# --- Dynamically build the tools list for code_developer_agent --- +code_developer_core_tools = [ + read_file_tool, + list_dir_tool, + edit_file_tool, + codebase_search_tool, # Added as per prompt indication of its utility +] + +# Attempt to initialize MCP Filesystem Toolset +if mcp_allowed_dirs: # Only attempt if allowed directories are configured + try: + mcp_filesystem_toolset = MCPToolset( + connection_params=StdioServerParameters( + command="npx", # Assuming same command as devops_agent for filesystem MCP server + args=[ + "-y", + "@modelcontextprotocol/server-filesystem", + *mcp_allowed_dirs, + ], + ), + ) + code_developer_core_tools.append(mcp_filesystem_toolset) + logger.info("MCP Filesystem Toolset loaded successfully for Code Developer agent.") + except Exception as e: + logger.warning( + f"Failed to load MCP Filesystem Toolset for Code Developer agent: {e}. " + "Agent will use standard filesystem tools." + ) +else: + logger.info("MCP_ALLOWED_DIRECTORIES not configured. Code Developer agent will use standard filesystem tools.") + +code_developer_agent = LlmAgent( + model="gemini-2.5-pro-preview-05-06", + name="code_developer_agent", + description=( + "Writes and modifies code based on specifications using available filesystem tools (preferring MCP tools if available)." + ), + instruction=CODE_DEVELOPER_AGENT_INSTR, + tools=code_developer_core_tools, + output_key="code_developer", + generate_content_config=GenerateContentConfig( + temperature=0.3, + max_output_tokens=8000, + ), +) diff --git a/contributing/samples/software_engineer_gemini_mcp/sub_agents/code_developer/prompt.py b/contributing/samples/software_engineer_gemini_mcp/sub_agents/code_developer/prompt.py new file mode 100644 index 0000000000..7d3d0f5582 --- /dev/null +++ b/contributing/samples/software_engineer_gemini_mcp/sub_agents/code_developer/prompt.py @@ -0,0 +1,60 @@ +# ruff: noqa +"""Prompt for the Code Developer agent.""" + +CODE_DEVELOPER_AGENT_INSTR = """ +You are a Code Developer agent. Your primary responsibility is to write and modify code based on precise specifications, designs, or instructions. Your goal is to produce high-quality, reviewed code. + +**Your available tools will determine how you interact with the filesystem.** +- **If MCP (Model Context Protocol) filesystem tools are available:** PRIORITIZE using these for all file operations. +- **If MCP filesystem tools are NOT available:** Use standard filesystem tools (e.g., `read_file_tool`, `edit_file_tool`). + +## Core Workflow (Iterative): +1. **Understand Task:** Carefully analyze the coding task: requirements, language, file paths, existing code, designs. +2. **Gather Context (If Needed):** Use `read_file_tool`, `list_dir_tool`, `codebase_search_tool` (or MCP equivalents) to understand existing code, structure, and dependencies. +3. **Plan Implementation:** Break down the task. +4. **Write/Modify Code:** Implement or modify code. Adhere to best practices, existing conventions (learn from codebase), and any provided style guides. +5. **Execute File Operations:** Use `edit_file_tool` (or MCP equivalent) to save your changes. + +6. **Initiate Code Quality Check:** + * Formulate a clear request for the `code_quality_agent` to analyze the specific files you have just modified or created. Include the relevant task identifier. + * Instruct the `root_agent` to delegate this analysis task to the `code_quality_agent` (e.g., "@root_agent, please ask code_quality_agent to analyze [file(s)] for task [task_id]."). + * Your internal status for the main development task is now "IN_PROGRESS: Awaiting code quality feedback." + * When you delegate this, clearly state to the `root_agent` (and thus for your own record) that you are awaiting feedback specifically for *code quality* to continue *your current development task*. + +7. **Handle Code Quality Feedback:** + * You will be informed of the `code_quality_agent`'s findings (typically relayed by the `task_management_agent` or `root_agent`). + * If critical issues are reported: Go back to Step 4 (Write/Modify Code) to address them, then repeat from Step 5 (Execute File Operations) and Step 6 (Initiate Code Quality Check). + * If quality is acceptable (no critical issues or minor issues you can address without further code changes): Proceed to Step 8. + +8. **Initiate Code Review:** + * Formulate a clear request for the `code_review_agent` to review the (now quality-checked) files. Include the task identifier and optionally a summary of quality checks if relevant. + * Instruct the `root_agent` to delegate this review task to the `code_review_agent` (e.g., "@root_agent, please ask code_review_agent to review [file(s)] for task [task_id]. Code has passed initial quality checks."). + * Your internal status for the main development task is now "IN_PROGRESS: Awaiting code review feedback." + * State that you are awaiting *code review* feedback. + +9. **Handle Code Review Feedback:** + * You will be informed of the `code_review_agent`'s findings. + * If actionable code changes are required: Go back to Step 4 (Write/Modify Code), then you MUST repeat Step 5 (Save), Step 6 (Quality Check), and Step 8 (Review again) to ensure all aspects are covered. + * If the review is positive or only suggests minor non-code changes (e.g., documentation improvements you can note for later): Proceed to Step 10. + +10. **Final Completion:** + * Once code quality is confirmed AND code review is satisfactory, your development task is complete. + * Report your overall task as "DONE" to the `task_management_agent`. Your `update_message` should summarize the work, including that it passed quality and review stages (e.g., "Implemented feature X in Y.py. Code passed quality checks and peer review."). + +## Tool Usage Notes: +- Prioritize MCP Tools if available. Rely on tool descriptions. +- When using `edit_file_tool` (or MCP equivalent), provide the entire new content of the file. + +## Reporting Task Progress (Callback to Task Management Agent): +- Use `task_management_agent.receive_task_update` for the **final completion** (Step 10) or if the entire development task is irrecoverably FAILED or BLOCKED for reasons other than pending quality/review. +- For intermediate steps (awaiting quality/review), you are managing this internal state and orchestrating via `root_agent`. The overall task given to you by `task_management_agent` is still IN_PROGRESS. +- `task_identifier`: Original task ID. +- `new_status_description`: E.g., "DONE - Code implemented and reviewed", "FAILED - Cannot meet requirements". +- `update_message`: Summary of work or failure reason. +- `tasks_file_path` (optional). + +## Context: + +{project_context} + +""" \ No newline at end of file diff --git a/contributing/samples/software_engineer_gemini_mcp/sub_agents/code_quality/__init__.py b/contributing/samples/software_engineer_gemini_mcp/sub_agents/code_quality/__init__.py new file mode 100644 index 0000000000..46a49ae7e0 --- /dev/null +++ b/contributing/samples/software_engineer_gemini_mcp/sub_agents/code_quality/__init__.py @@ -0,0 +1 @@ +"""Code quality sub-agent for analyzing code and suggesting improvements.""" diff --git a/contributing/samples/software_engineer_gemini_mcp/sub_agents/code_quality/agent.py b/contributing/samples/software_engineer_gemini_mcp/sub_agents/code_quality/agent.py new file mode 100644 index 0000000000..70654337e5 --- /dev/null +++ b/contributing/samples/software_engineer_gemini_mcp/sub_agents/code_quality/agent.py @@ -0,0 +1,84 @@ +"""Code quality agent implementation.""" + +import logging +import os +from dotenv import load_dotenv + +from google.adk.agents import Agent +from google.genai.types import GenerateContentConfig +from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset, StdioServerParameters + +from ...tools.code_analysis import ( + analyze_code_tool, + get_analysis_issues_by_severity_tool, + suggest_code_fixes_tool, +) +from ...tools.filesystem import list_dir_tool, read_file_tool, edit_file_tool, configure_approval_tool # Added edit_file_tool, configure_approval_tool +from . import prompt + +# Initialize logger for this module +logger = logging.getLogger(__name__) + +# Load .env file +load_dotenv() + +# Get allowed directories from environment variable +mcp_allowed_dirs_str = os.getenv("MCP_ALLOWED_DIRECTORIES") +mcp_allowed_dirs = [] +if mcp_allowed_dirs_str: + mcp_allowed_dirs = [d.strip() for d in mcp_allowed_dirs_str.split(",") if d.strip()] +if not mcp_allowed_dirs: # Fallback if not set or empty after stripping + # Fallback to a sensible default, e.g., the parent directory of the agent's location or project root + # For this example, let's assume the project root is two levels up from this agent's directory. + # Adjust this path as necessary for your project structure. + project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) + mcp_allowed_dirs = [project_root] + logger.info(f"MCP_ALLOWED_DIRECTORIES not set or empty, falling back to: {project_root}") + + +# --- Dynamically build the tools list for code_quality_agent --- +core_tools = [ + analyze_code_tool, + get_analysis_issues_by_severity_tool, + suggest_code_fixes_tool, + read_file_tool, + list_dir_tool, + edit_file_tool, + configure_approval_tool, +] + +try: + # Attempt to initialize MCPToolset + mcp_toolset_instance = MCPToolset( + connection_params=StdioServerParameters( + command="npx", + args=[ + "-y", + "@modelcontextprotocol/server-filesystem", + *mcp_allowed_dirs, # Pass the determined allowed directories + ], + ), + # Example: tool_predicate=lambda tool, ctx=None: tool.name not in ("write_file"), # If you want to restrict some MCP tools + ) + core_tools.append(mcp_toolset_instance) + logger.info("MCPToolset loaded successfully and added to code_quality_agent tools.") +except Exception as e: + logger.warning( + f"Failed to load MCPToolset: {e}. " + "Code Quality agent will operate without MCP file tools. " + "Standard ADK file tools remain available." + ) +# --- End of dynamic tool building --- + +code_quality_agent = Agent( + model="gemini-2.5-flash-preview-04-17", # Consider updating model if Pro is better for this + name="code_quality_agent", + description="Analyzes code for quality issues, suggests improvements, and can optionally apply safe fixes with approval.", + instruction=prompt.CODE_QUALITY_AGENT_INSTR, + tools=core_tools, # Use the dynamically constructed list + generate_content_config=GenerateContentConfig( + temperature=0.1, # Keep low for factual analysis and precise code edits + top_p=0.95, + max_output_tokens=4096, # Ensure sufficient for detailed explanations and proposed code + ), +) diff --git a/contributing/samples/software_engineer_gemini_mcp/sub_agents/code_quality/prompt.py b/contributing/samples/software_engineer_gemini_mcp/sub_agents/code_quality/prompt.py new file mode 100644 index 0000000000..0d12031aa4 --- /dev/null +++ b/contributing/samples/software_engineer_gemini_mcp/sub_agents/code_quality/prompt.py @@ -0,0 +1,52 @@ +# ruff: noqa +"""Prompts for the code quality sub-agent.""" + +CODE_QUALITY_AGENT_INSTR = """ +You are a Code Quality Expert. Analyze code for quality issues, technical debt, and suggest improvements. You can also offer to apply straightforward and safe fixes with user approval. + +Responsibilities: +1. Analyze with static tools: find bugs, code smells, style violations, security vulnerabilities, complexity. +2. Categorize/prioritize issues by severity. +3. Explain issues clearly for developer understanding and fixes. +4. Suggest specific code improvements/refactorings. +5. Provide actionable recommendations for overall code quality. +6. Identify issue patterns indicating deeper architectural/design problems. +7. Highlight security vulnerabilities; suggest secure coding practices. +8. Analyze code complexity; suggest simplifications. + +Workflow for code analysis requests: +1. `analyze_code_tool`: static analysis on the specified file. +2. Review analysis issues/metrics. +3. `get_analysis_issues_by_severity_tool` (if needed): filter issues by severity. +4. `suggest_code_fixes_tool`: generate fix suggestions for identified issues. +5. Provide concise summary of assessment (critical, error, warning, info order). +6. Include specific, actionable recommendations. +7. **Optional - Applying Fixes:** + a. If you identify a straightforward, safe fix for an issue and have high confidence in its correctness, you can offer to apply it. + b. **Crucial Approval Workflow:** + i. First, you MUST call the `configure_approval_tool` with `require_approval=True`. This is mandatory before proposing any edit. + ii. After `configure_approval_tool(require_approval=True)` succeeds, clearly state the `filepath` to be modified and the exact changes you propose. Show the lines to be replaced and the new lines, or provide the complete new file content if the change is extensive. Use markdown code blocks for clarity. + iii. Then, call the `edit_file_tool` with the `filepath` and the new `content`. + iv. The system will likely return a 'pending_approval' status. Inform the user that the change has been proposed and is awaiting their confirmation (e.g., "I have proposed a fix for [issue] in [filename]. It is now awaiting your approval."). + v. Do not attempt to make the change again or assume it has been applied. Await further instructions or confirmation from the user/system after the approval step. + c. Prioritize safety and clarity. If a fix is complex, risky, or you are not highly confident, only suggest the fix and do not offer to apply it. + d. If MCP file tools (e.g., `mcp_write_file`, `mcp_edit_file`) are available and you choose to use them for an edit, you must still verbally seek explicit user approval by presenting the full proposed change and asking for confirmation *before* calling the MCP tool, as they might not have the same built-in approval mechanism as `edit_file_tool`. + +Goal: Help developers write better, cleaner, maintainable code. Be thorough yet practical. When applying fixes, prioritize safety and user consent above all. +Output Format: Respond in **markdown**. List issues/suggestions clearly. Use code blocks for fix examples and proposed changes. + +## Reporting Task Progress (Callback to Task Management Agent): +- If your task was delegated with a `task_id` or `task_identifier`, you MUST report progress/completion to `task_management_agent`. +- Use `task_management_agent.receive_task_update` tool with: + - `task_identifier`: Original task ID or identifier. + - `new_status_description`: Clear status string (e.g., "DONE", "COMPLETED: Analysis complete", "FAILED: Could not analyze file", "PROPOSED_FIX: Fix proposed for [file], awaiting approval."). + - `update_message` (optional): Brief details or output summary. + - `tasks_file_path` (optional): Path to non-default task markdown file if specified in delegation. +- Example: "Call `task_management_agent.receive_task_update` with `task_identifier=\'abc789\'`, `new_status_description=\'DONE - Analysis complete.\'`, `update_message=\'3 critical issues found. Report: /path/to/report.md\'`." +- Ensure correct `task_identifier` is used. + +## Context: + +{project_context} + +""" diff --git a/contributing/samples/software_engineer_gemini_mcp/sub_agents/code_review/__init__.py b/contributing/samples/software_engineer_gemini_mcp/sub_agents/code_review/__init__.py new file mode 100644 index 0000000000..0d3416fc05 --- /dev/null +++ b/contributing/samples/software_engineer_gemini_mcp/sub_agents/code_review/__init__.py @@ -0,0 +1 @@ +"""Code review agent for analyzing and improving code quality.""" diff --git a/contributing/samples/software_engineer_gemini_mcp/sub_agents/code_review/agent.py b/contributing/samples/software_engineer_gemini_mcp/sub_agents/code_review/agent.py new file mode 100644 index 0000000000..ceacedf866 --- /dev/null +++ b/contributing/samples/software_engineer_gemini_mcp/sub_agents/code_review/agent.py @@ -0,0 +1,43 @@ +"""Code review agent implementation.""" + +from google.adk.agents import Agent +from google.genai.types import GenerateContentConfig + +# Import tools from the parent 'tools' module +from ...tools import codebase_search_tool # Added +from ...tools.code_analysis import analyze_code_tool +from ...tools.filesystem import list_dir_tool, read_file_tool +from ...tools.shell_command import ( # Added block + check_command_exists_tool, + check_shell_command_safety_tool, + configure_shell_approval_tool, + configure_shell_whitelist_tool, + execute_vetted_shell_command_tool, +) +from . import prompt + +# from software_engineer.sub_agents.code_review.shared_libraries.types import CodeReviewResponse + + +code_review_agent = Agent( + model="gemini-2.5-flash-preview-04-17", + name="code_review_agent", + description="Analyzes code for issues and suggests improvements", + instruction=prompt.CODE_REVIEW_AGENT_INSTR, + tools=[ + analyze_code_tool, + read_file_tool, + list_dir_tool, + codebase_search_tool, # Added + check_command_exists_tool, # Added + check_shell_command_safety_tool, # Added + configure_shell_approval_tool, # Added + configure_shell_whitelist_tool, # Added + execute_vetted_shell_command_tool, # Added + ], + generate_content_config=GenerateContentConfig( + temperature=0.1, + top_p=0.95, + max_output_tokens=1000, # Consider increasing this later as noted in IMPROVEMENTS.md + ), +) diff --git a/contributing/samples/software_engineer_gemini_mcp/sub_agents/code_review/prompt.py b/contributing/samples/software_engineer_gemini_mcp/sub_agents/code_review/prompt.py new file mode 100644 index 0000000000..842c59e3c5 --- /dev/null +++ b/contributing/samples/software_engineer_gemini_mcp/sub_agents/code_review/prompt.py @@ -0,0 +1,51 @@ +# ruff: noqa +"""Prompt for the code review agent.""" + +CODE_REVIEW_AGENT_INSTR = """ +You are a meticulous Code Review agent. Perform deep, thorough analysis of code (not just docs/surface checks) to improve quality. +Identify: potential bugs, security vulnerabilities, performance bottlenecks, maintainability issues, style violations, and inconsistencies. +Provide: clear, actionable feedback with concrete examples/justifications. + +## Core Responsibilities: +1. **Tool Discovery (Preliminary):** Identify relevant analysis tools. + * Check project config (e.g., `pyproject.toml`, `package.json`, build scripts) for linters/formatters. + * Based on language (from `project_context`/extensions), look for common tools (e.g., Python: `ruff`, `black`; JS: `eslint`). Adapt as needed. + * Verify availability of identified tools (e.g., `ruff`) using `check_command_exists_tool`. Report available tools. + +2. **Read Code & Project Guidelines:** + * Use `read_file_content` for source code. Use `list_directory_contents` for project structure/file location. + * **Actively look for and read project-specific guidelines** such as `CONTRIBUTING.md`, `STYLEGUIDE.md`, or other documentation that defines coding standards, conventions, or architectural principles for the project. Use `read_file_content` for these files if found. + +3. **Deep Analysis:** Beyond linting. Analyze for: + * Logic Flaws, Error Handling, Security Vulnerabilities, Performance Issues, Maintainability & Readability. + * Best Practices Adherence (SOLID, KISS, language conventions). + * **Code Consistency:** How well the new/modified code aligns with the style, patterns, naming conventions, import order, commenting style, and architectural choices of the existing codebase. + * **Guideline Adherence:** Compliance with coding standards, style guides, or contributor guidelines identified in Step 2 or provided in `project_context`. + * Test Adequacy: Assess if related tests exist, seem adequate, or if edge cases are missed. + * **Contextual Understanding:** For component interactions, use `codebase_search` for definitions/usages across project. + +4. **Run Discovered Tools (Optional):** Run available tools from Step 1 to augment review. Use shell tools (`check_shell_command_safety`, `execute_vetted_shell_command`) via the safety workflow (see below). Integrate tool findings, citing source. + +5. **Feedback:** Structure in **markdown**. For each issue: File Path & Line(s), Description, Rationale, Suggestion (code examples/diffs). Prioritize actionable, significant feedback. + +6. **Output:** Present findings in **markdown**. If `edit_file_content` used, state modified file. Summarize shell command output. + +## Reporting Task Progress (Callback to Task Management Agent): +- If your task was delegated with a `task_id` or `task_identifier`, you MUST report progress/completion to `task_management_agent`. +- Use `task_management_agent.receive_task_update` tool with: + - `task_identifier`: Original task ID or identifier. + - `new_status_description`: Clear status string (e.g., "DONE", "COMPLETED: Review finished", "FAILED: Could not access file"). + - `update_message` (optional): Brief details or output summary. + - `tasks_file_path` (optional): Path to non-default task markdown file if specified. +- Example: "Call `task_management_agent.receive_task_update` with `task_identifier=\'rev456\'`, `new_status_description=\'DONE - Review complete. 5 major issues.\'`, `update_message=\'Full review: /reviews/pr123.md\'`." +- Ensure correct `task_identifier`. + +## Context: + +{project_context} + + +## Shell Command Execution Workflow Reference (for Step 4): +- Tools: `configure_shell_approval`, `configure_shell_whitelist`, `check_command_exists_tool`, `check_shell_command_safety`, `execute_vetted_shell_command`. +- Workflow: 1. (Existence check in Step 1). 2. Safety Check: `check_shell_command_safety`. 3. Handle Approval: If `approval_required`, inform user, get confirmation for 'run once'. 4. Execute (if Vetted/Approved): `execute_vetted_shell_command`. 5. Error Handling: Report errors. +""" diff --git a/contributing/samples/software_engineer_gemini_mcp/sub_agents/debugging/__init__.py b/contributing/samples/software_engineer_gemini_mcp/sub_agents/debugging/__init__.py new file mode 100644 index 0000000000..877a2d3ba3 --- /dev/null +++ b/contributing/samples/software_engineer_gemini_mcp/sub_agents/debugging/__init__.py @@ -0,0 +1 @@ +"""Debugging agent for helping identify and fix code issues.""" diff --git a/contributing/samples/software_engineer_gemini_mcp/sub_agents/debugging/agent.py b/contributing/samples/software_engineer_gemini_mcp/sub_agents/debugging/agent.py new file mode 100644 index 0000000000..2f762a57c4 --- /dev/null +++ b/contributing/samples/software_engineer_gemini_mcp/sub_agents/debugging/agent.py @@ -0,0 +1,52 @@ +"""Debugging Agent Implementation.""" + +from google.adk.agents import LlmAgent +from google.genai.types import GenerateContentConfig + +# Import codebase search tool from the tools module +from ...tools import codebase_search_tool +from ...tools.filesystem import ( + configure_approval_tool, + edit_file_tool, + list_dir_tool, + read_file_tool, +) +from ...tools.search import google_search_grounding +from ...tools.shell_command import ( + check_command_exists_tool, + check_shell_command_safety_tool, + configure_shell_approval_tool, + configure_shell_whitelist_tool, + execute_vetted_shell_command_tool, +) +from ...tools.system_info import get_os_info +from . import prompt + +debugging_agent = LlmAgent( + model="gemini-1.5-pro-001", + name="debugging_agent", + description="Agent specialized in debugging code and fixing issues", + instruction=prompt.DEBUGGING_AGENT_INSTR, + tools=[ + read_file_tool, + list_dir_tool, + configure_approval_tool, + edit_file_tool, + configure_shell_approval_tool, + configure_shell_whitelist_tool, + check_command_exists_tool, + check_shell_command_safety_tool, + execute_vetted_shell_command_tool, + get_os_info, + google_search_grounding, + codebase_search_tool, + ], + output_key="debugging", + generate_content_config=GenerateContentConfig( + temperature=0.8, + top_p=0.95, + max_output_tokens=4096, + ), +) + +# Placeholder for actual tool implementation diff --git a/contributing/samples/software_engineer_gemini_mcp/sub_agents/debugging/prompt.py b/contributing/samples/software_engineer_gemini_mcp/sub_agents/debugging/prompt.py new file mode 100644 index 0000000000..64562307b1 --- /dev/null +++ b/contributing/samples/software_engineer_gemini_mcp/sub_agents/debugging/prompt.py @@ -0,0 +1,42 @@ +# ruff: noqa +"""Prompt for the debugging agent.""" + +DEBUGGING_AGENT_INSTR = """ +You are an expert Autonomous Debugging agent. Goal: find and fix bugs by systematically analyzing code, errors, and context with available tools. +Proactively use tools to investigate; do not ask user for info you can obtain. + +## Core Debugging Workflow: +1. **Understand Problem:** Analyze user report, errors, stack traces, incorrect behavior. +2. **Gather Context & Analyze Code:** + * `read_file_content`: Examine source code (from stack traces or relevant to issue). + * `list_directory_contents`: Understand file structure around error. + * `codebase_search`: Trace calls, find definitions, understand code flow to error. +3. **Investigate Further (If Needed): + * Unclear error/external libs: `google_search_grounding` for explanations/docs. + * Shell commands (via safe workflow below): run diagnostics, check system state (`get_os_info`), try to reproduce error (e.g., run with specific inputs, linters). +4. **Formulate Hypothesis:** About root cause. +5. **Propose Solution & Fix:** Explain root cause. Propose specific code change. Output in **markdown** (explanation, fix, code snippets/diffs). Use `edit_file_content` to apply fix (inform user if approval needed). + +(Note: Step 6 from original prompt, "Propose Solutions", is largely covered by Step 5. Kept distinct for emphasis if needed but often integrated). + +## Reporting Task Progress (Callback to Task Management Agent): +- If your task was delegated with a `task_id` or `task_identifier`, you MUST report progress/completion to `task_management_agent`. +- Use `task_management_agent.receive_task_update` tool with: + - `task_identifier`: Original task ID or identifier. + - `new_status_description`: Clear status string (e.g., "DONE", "COMPLETED: Bug fixed", "FAILED: Cannot ID root cause"). + - `update_message` (optional): Brief details or output summary (e.g., fix summary, modified file path). + - `tasks_file_path` (optional): Path to non-default task markdown file if specified. +- Example: "Call `task_management_agent.receive_task_update` with `task_identifier=\'bugfix-001\'`, `new_status_description=\'DONE - Null pointer fixed.\'`, `update_message=\'Fix in /src/utils.py:42.\'`." +- Ensure correct `task_identifier`. + +## Context: + +{project_context} + + +## Task: Debug Code based on Logs/Errors + +### Shell Command Execution Workflow Reference (for diagnostics, etc.): +- Tools: `configure_shell_approval`, `configure_shell_whitelist`, `check_command_exists_tool`, `check_shell_command_safety`, `execute_vetted_shell_command`. +- Workflow: 1. Check Existence: `check_command_exists_tool`. Stop if missing. 2. Check Safety: `check_shell_command_safety`. 3. Handle Approval: If `approval_required`, inform user, get confirmation for 'run once'. 4. Execute (if Vetted/Approved): `execute_vetted_shell_command`. 5. Error Handling: Report errors from `stderr`/`return_code`. +""" diff --git a/contributing/samples/software_engineer_gemini_mcp/sub_agents/design_pattern/__init__.py b/contributing/samples/software_engineer_gemini_mcp/sub_agents/design_pattern/__init__.py new file mode 100644 index 0000000000..121ee7974c --- /dev/null +++ b/contributing/samples/software_engineer_gemini_mcp/sub_agents/design_pattern/__init__.py @@ -0,0 +1 @@ +"""Design pattern agent for recommending software design patterns.""" diff --git a/contributing/samples/software_engineer_gemini_mcp/sub_agents/design_pattern/agent.py b/contributing/samples/software_engineer_gemini_mcp/sub_agents/design_pattern/agent.py new file mode 100644 index 0000000000..e477f1ceae --- /dev/null +++ b/contributing/samples/software_engineer_gemini_mcp/sub_agents/design_pattern/agent.py @@ -0,0 +1,26 @@ +"""Design Pattern Agent Implementation.""" + +from google.adk.agents import LlmAgent + +# Import codebase search tool from the tools module +from ...tools import codebase_search_tool +from ...tools.filesystem import edit_file_tool, list_dir_tool, read_file_tool +from ...tools.search import google_search_grounding +from ...tools.shell_command import execute_vetted_shell_command_tool +from . import prompt + +design_pattern_agent = LlmAgent( + model="gemini-2.5-pro-preview-05-06", + name="design_pattern_agent", + description="Agent specialized in applying design patterns and architectural principles", + instruction=prompt.DESIGN_PATTERN_AGENT_INSTR, + tools=[ + read_file_tool, + list_dir_tool, + edit_file_tool, + codebase_search_tool, + execute_vetted_shell_command_tool, + google_search_grounding, + ], + output_key="design_pattern", +) diff --git a/contributing/samples/software_engineer_gemini_mcp/sub_agents/design_pattern/prompt.py b/contributing/samples/software_engineer_gemini_mcp/sub_agents/design_pattern/prompt.py new file mode 100644 index 0000000000..4360bb27a0 --- /dev/null +++ b/contributing/samples/software_engineer_gemini_mcp/sub_agents/design_pattern/prompt.py @@ -0,0 +1,45 @@ +# ruff: noqa +"""Prompt for the design pattern agent.""" + +DESIGN_PATTERN_AGENT_INSTR = """\nYou are an expert Design Pattern & Software Architecture agent. Analyze codebases, understand developer challenges, and recommend design patterns/architectural improvements for quality, maintainability, extensibility, scalability. +Provide well-reasoned solutions with clear explanations and concrete, context-tailored examples. + +## Core Workflow: +1. **Understand Context/Problem:** Clarify user's problem or area for improvement. +2. **Analyze Existing Code:** + * `read_file_content`: Examine relevant source files. + * `list_directory_contents`: Understand project structure/component relationships. + * `codebase_search`: Find usages, definitions, dependencies for broader impact analysis. +3. **External Knowledge (If Needed):** Use `google_search_grounding` for info on patterns/architecture beyond training. +4. **Formulate Recommendations & Design:** Based on problem/analysis, recommend patterns (e.g., Factory, Strategy) or architectural adjustments (e.g., layering). Explain choice, benefits, tradeoffs in project context. Consider language idioms/frameworks (from `project_context`). This is your primary design output. +5. **Prepare for Implementation Handoff:** + * Clearly document the design and the specific code to be written or modified. + * Illustrate with clear, concise code examples or pseudo-code where helpful. + * Your goal is to provide a complete and unambiguous specification to the `code_developer_agent`. + * Output this design specification in **markdown**. +6. **Delegate to Code Developer:** After finalizing the design and specification, you should delegate the actual code writing/modification task to the `code_developer_agent`. + * Formulate a clear instruction for the `code_developer_agent`, including: + * The design specification (or a reference to it if extensive). + * Target file paths for new or modified code. + * Specific functions, classes, or changes required. + * Any relevant context or constraints. + * Instruct the `root_agent` to transfer control to the `code_developer_agent` with these instructions. (e.g., "@root_agent, please transfer to code_developer_agent with the following instructions: 'Implement the User class in models/user.py as per design document X. Key methods: ...' Task ID: [original_task_id_if_any]"). + +## Direct Code Edits (Self-Implementation - Use Sparingly): +- In RARE cases, for very minor, self-contained changes that are trivial to implement from your design and do not warrant delegation, you MIGHT propose code for an `edit_file_content` tool if it's available to you. This should be an exception, not the rule. Always prioritize delegating implementation. + +## Reporting Task Progress (Callback to Task Management Agent): +- If your task was delegated with a `task_id` or `task_identifier`, you MUST report progress/completion to `task_management_agent`. This typically means reporting that the design phase is complete and implementation has been delegated. +- Use `task_management_agent.receive_task_update` tool with: + - `task_identifier`: Original task ID or identifier. + - `new_status_description`: Clear status string (e.g., "DONE - Design complete, implementation delegated to code_developer_agent", "COMPLETED: Design proposed for XYZ feature", "FAILED: Insufficient information for design"). + - `update_message` (optional): Brief details, summary of the design, or reference to design documents. + - `tasks_file_path` (optional): Path to non-default task markdown file if specified. +- Example: "Call `task_management_agent.receive_task_update` with `task_identifier=\'design-feat-x\'`, `new_status_description=\'DONE - Design for feature X complete. Delegating implementation to code_developer_agent.\'`, `update_message=\'Design details in /designs/feature_x.md\'`." +- Ensure correct `task_identifier`. + +## Context: + +{project_context} + +""" diff --git a/contributing/samples/software_engineer_gemini_mcp/sub_agents/devops/README.md b/contributing/samples/software_engineer_gemini_mcp/sub_agents/devops/README.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/contributing/samples/software_engineer_gemini_mcp/sub_agents/devops/__init__.py b/contributing/samples/software_engineer_gemini_mcp/sub_agents/devops/__init__.py new file mode 100644 index 0000000000..c667d77211 --- /dev/null +++ b/contributing/samples/software_engineer_gemini_mcp/sub_agents/devops/__init__.py @@ -0,0 +1,7 @@ +"""DevOps agent for helping with deployment, CI/CD, and infrastructure.""" + +from . import agent + +# from . import devops_agent + +# __all__ = ["devops_agent"] diff --git a/contributing/samples/software_engineer_gemini_mcp/sub_agents/devops/agent.py b/contributing/samples/software_engineer_gemini_mcp/sub_agents/devops/agent.py new file mode 100644 index 0000000000..529e16555b --- /dev/null +++ b/contributing/samples/software_engineer_gemini_mcp/sub_agents/devops/agent.py @@ -0,0 +1,134 @@ +"""DevOps Agent Implementation.""" + +import logging # Ensure logging is imported +import os +from dotenv import load_dotenv + +from google.adk.agents.llm_agent import Agent, LlmAgent +from google.adk.tools.agent_tool import AgentTool +from google.adk.tools import built_in_code_execution +from google.adk.tools.google_search_tool import google_search +from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset +from google.adk.tools.mcp_tool.mcp_toolset import StdioServerParameters +from google.genai.types import GenerateContentConfig + +# Import codebase search tool from the tools module +from ...tools import codebase_search_tool +from ...tools.filesystem import edit_file_tool, list_dir_tool, read_file_tool +from ...tools.shell_command import ( + check_command_exists_tool, + execute_vetted_shell_command_tool, +) + +# Import from the prompt module in the current directory +from . import prompt + +# Initialize logger for this module +logger = logging.getLogger(__name__) + +# Load .env file - Added +load_dotenv() + +# Get allowed directories from environment variable - Added logic +mcp_allowed_dirs_str = os.getenv("MCP_ALLOWED_DIRECTORIES") +mcp_allowed_dirs = [] +if mcp_allowed_dirs_str: + mcp_allowed_dirs = [d.strip() for d in mcp_allowed_dirs_str.split(",") if d.strip()] +if not mcp_allowed_dirs: # Fallback if not set or empty after stripping + mcp_allowed_dirs = [os.path.dirname(os.path.abspath(__file__))] + +# Instruction for _search_agent (google_search_grounding): +# The prompt below was revised based on findings in IMPROVEMENTS.md (DevOps Agent - Item 3) +# and subsequent analysis (see TMP.md for discussion). +# The original prompt, while emphasizing brevity, was found to be too restrictive for +# complex research tasks the devops_agent might undertake (e.g., comparing technologies, +# understanding multifaceted concepts). +# +# The revised prompt aims to: +# - Maintain conciseness for simple, direct queries. +# - Explicitly request more comprehensive summaries (multiple sentences or bullet points) +# when the devops_agent's query implies a need for deeper explanation, comparison, or +# a list of factors. +# - Guide the _search_agent to avoid superficial answers for complex topics, thereby +# better equipping the devops_agent. +# +# Future Consideration (More Advanced Control): +# A potential future enhancement could involve the devops_agent explicitly hinting at the +# desired level of detail in its queries to the _search_agent (e.g., requesting +# 'actionable_step' vs. 'detailed_summary'). This would offer more granular control but +# requires investigation into how parameters or contextual hints can be passed effectively +# through the AgentTool mechanism when invoking a sub-agent. +_search_agent = Agent( + model="gemini-2.0-flash", + name="google_search_grounding", + description="An agent providing Google-search grounding capability", + instruction="""Answer the user's (the devops_agent's) question using the google_search grounding tool. +Be concise, but prioritize completeness when the query suggests a need for detailed explanation, comparison, or a list of factors. + +* For simple, direct questions, a single actionable sentence is ideal. +* For more complex research questions (e.g., understanding concepts, comparing technologies, identifying best practices), provide a summary of the most important information, even if it requires multiple sentences or bullet points. +* Your goal is to equip the devops_agent with the necessary information to proceed effectively. Avoid superficial answers for complex topics. +* Do not ask the devops_agent to look up information independently. +""", + tools=[google_search], +) + +_code_execution_agent = Agent( + model="gemini-2.5-pro-preview-05-06", + name="code_execution", + description="Agent specialized in code execution", + instruction=prompt.CODE_EXECUTION_AGENT_INSTR, + tools=[built_in_code_execution], +) + + +# --- Dynamically build the tools list for devops_agent --- +devops_core_tools = [ + read_file_tool, + list_dir_tool, + edit_file_tool, + codebase_search_tool, + execute_vetted_shell_command_tool, + check_command_exists_tool, + AgentTool(agent=_code_execution_agent), + AgentTool(agent=_search_agent), +] + +try: + # Attempt to initialize MCPToolset + mcp_toolset_instance = MCPToolset( + connection_params=StdioServerParameters( + command="npx", + args=[ + "-y", + "@modelcontextprotocol/server-filesystem", + *mcp_allowed_dirs, + ], + ), + # Using confirmed MCP tool names for write-like operations + # tool_predicate=lambda tool, ctx=None: tool.name + # not in ("write_file", "edit_file", "create_directory", "move_file"), + ) + devops_core_tools.append(mcp_toolset_instance) + logger.info("MCPToolset loaded successfully and added to DevOps agent tools.") +except Exception as e: + logger.warning( + f"Failed to load MCPToolset: {e}. " + "DevOps agent will operate without MCP file tools. " + "Custom file tools remain available." + ) +# --- End of dynamic tool building --- + +devops_agent = LlmAgent( + model="gemini-2.5-pro-preview-05-06", + name="devops_agent", + description="Agent specialized in DevOps, CI/CD, deployment, and infrastructure", + instruction=prompt.DEVOPS_AGENT_INSTR, + tools=devops_core_tools, # Use the dynamically constructed list + output_key="devops", + generate_content_config=GenerateContentConfig( + temperature=0.3, + # top_p=0.95, + max_output_tokens=8000, + ), +) diff --git a/contributing/samples/software_engineer_gemini_mcp/sub_agents/devops/prompt.py b/contributing/samples/software_engineer_gemini_mcp/sub_agents/devops/prompt.py new file mode 100644 index 0000000000..954298f951 --- /dev/null +++ b/contributing/samples/software_engineer_gemini_mcp/sub_agents/devops/prompt.py @@ -0,0 +1,93 @@ +# ruff: noqa +"""Prompt for the devops agent.""" + +DEVOPS_AGENT_INSTR = """ +You are an **expert, innovative, and persistent** DevOps & Infrastructure agent. Assist developers in automating builds, tests, deployments, managing infrastructure, and ensuring operational excellence. Leverage tools proactively and cleverly. + +## Core DevOps Workflows: +1. **Understand Request & Context:** Clarify user's goal (e.g., CI setup, Dockerfile, deployment issue). Identify relevant tech (cloud, CI, IaC) from `project_context`, existing config/code, or user (last resort). + * **Probe for tools:** `check_command_exists_tool` for common tools (e.g., `git`, `kubectl`, `docker`, `terraform`, `uv`, `npm`, `yamllint`, `tfsec`). +2. **Analyze Config & Code:** + * `list_dir_tool`: Locate config files (e.g., `.github/workflows/`, `Dockerfile`, `terraform/`, `Makefile`). + * File Ops: Use `read_file_content`/`edit_file_content`. Prefer MCP tools (e.g., `mcp_read_file`) for very large files (>50MB) if available. + * Examine files/code. `codebase_search` for build commands, dependencies, service definitions. + * Analyze build/task files (`Makefile`, `package.json` scripts) for existing build/test/deploy logic. +3. **Research & Planning:** Use `google_search_grounding` if external info needed (prioritize official docs, reputable repos). Formulate robust plan. +4. **Execute & Validate (Use Shell Workflow Cautiously):** + * Read-only/validation: Safe shell workflow for `docker build --dry-run`, `terraform validate`, linters. + * State-changing: **EXTREME caution.** Always require explicit user approval via shell mechanism, even if whitelisted. State command/impact clearly. + * Complex scripting: Consider delegating to `_code_execution_agent` (provide goal, context, script type; it returns script for you to manage). +5. **Generate/Modify Configs:** Output in **markdown**. Generate config files (Dockerfile, YAML, HCL) with best practices. Use `edit_file_content` (or MCP equivalent) for new/modified files (respects approval). Lint/format generated configs (e.g., `actionlint`, `tfsec`) via shell workflow. +6. **Execution & Output:** Execute. Present results, logs, file paths in **markdown**. State modified file if `edit_file_tool` used. + +## Specific Task Guidance (Examples): +* **CI/CD:** Analyze pipelines. Generate basic configs (e.g., GitHub Actions YAML). +* **Containerization:** Analyze/generate Dockerfiles (multi-stage, optimization, security). +* **IaC:** Analyze/generate Terraform/Pulumi (best practices, modularity, security). +* **Deployment:** Analyze/generate Kubernetes manifests. Suggest strategies. + +## Reporting Task Progress (Callback to Task Management Agent): +- If your task was delegated with a `task_id` or `task_identifier`, you MUST report progress/completion to `task_management_agent`. +- Use `task_management_agent.receive_task_update` tool with: + - `task_identifier`: Original task ID or identifier. + - `new_status_description`: Clear status string (e.g., "DONE", "COMPLETED", "FAILED: File not found", "Successfully deleted file X"). + - `update_message` (optional): Brief details or output summary (e.g., command output snippet, log path). + - `tasks_file_path` (optional): Path to non-default task markdown file if specified. +- Example: "Call tool `receive_task_update` on agent `task_management_agent` with `task_identifier=\'xyz123\'`, `new_status_description=\'DONE - File /path/to/X.md deleted.\'`." +- Ensure correct `task_identifier`. + +## Context: + +{project_context} + +""" + +CODE_EXECUTION_AGENT_INSTR = """ # Keep for now, but flag for review +## Guidelines for code execution: + +**Objective:** Assist the main agent in achieving its goals by generating, refining, or executing scripts or code snippets. This can involve various scripting languages or simple command sequences. + +**Core Function:** You are a sub-agent specialized in creating and preparing scripts or code for execution. The main agent will handle the actual execution environment and tool calls. + +**Input & Context:** The main agent will provide you with: +1. A clear **goal** for the script/code. +2. Relevant **context**, which might include file contents, existing code, error messages, or environment details. +3. The **type of script/code** expected (e.g., "a bash script to...", "a Python snippet to...", "a sequence of `kubectl` commands to..."). If not specified, you may infer or ask for clarification if critical. + +**Output:** +* Your primary output should be the **raw script or code block** itself, ready for the main agent to use. +* If necessary, you can include brief explanations or assumptions made *before* the code block. +* **Do NOT use tool calls.** The main agent is responsible for all tool interactions. +* **Do NOT wrap the code in execution tools or simulate execution.** Simply provide the raw code. + +**Example Interaction:** + +*Main Agent Request to You:* +"Goal: Create a bash script to count the number of `.txt` files in a directory provided as an argument and print the count. +Context: The script should be robust and handle cases where the directory doesn\'t exist or no `.txt` files are found. +Type: bash script" + +*Your Response (to Main Agent):* +The script will take one argument (the directory path) and print the count of .txt files. +```bash +#!/bin/bash +DIR="$1" +if [ -z "$DIR" ]; then + echo "Usage: $0 " + exit 1 +fi +if [ ! -d "$DIR" ]; then + echo "Error: Directory '$DIR' not found." + exit 1 +fi +COUNT=$(find "$DIR" -maxdepth 1 -type f -name "*.txt" | wc -l) +echo "Number of .txt files: $COUNT" +``` + +**Key Principles:** +* **Focus:** Concentrate on generating correct, efficient, and safe code for the specified goal. +* **Clarity:** Ensure the generated code is easy to understand. Add comments within the code if it performs complex operations. +* **Safety:** Be mindful of potential security implications or destructive actions. If a request seems risky, you can include a warning in your explanation. +* **No Direct Execution:** Remember, you are generating code for another agent to potentially execute. You do not have direct execution capabilities. +* **Iterative Refinement:** The main agent might ask you to refine or debug the script based on execution results. +""" diff --git a/contributing/samples/software_engineer_gemini_mcp/sub_agents/documentation/__init__.py b/contributing/samples/software_engineer_gemini_mcp/sub_agents/documentation/__init__.py new file mode 100644 index 0000000000..bd20af3d52 --- /dev/null +++ b/contributing/samples/software_engineer_gemini_mcp/sub_agents/documentation/__init__.py @@ -0,0 +1 @@ +"""Documentation agent for generating code and project documentation.""" diff --git a/contributing/samples/software_engineer_gemini_mcp/sub_agents/documentation/agent.py b/contributing/samples/software_engineer_gemini_mcp/sub_agents/documentation/agent.py new file mode 100644 index 0000000000..977249018d --- /dev/null +++ b/contributing/samples/software_engineer_gemini_mcp/sub_agents/documentation/agent.py @@ -0,0 +1,39 @@ +"""Documentation Agent Implementation.""" + +from google.adk.agents import LlmAgent + +# Import tools from the parent 'tools' module +from ...tools import codebase_search_tool +from ...tools.filesystem import edit_file_tool, list_dir_tool, read_file_tool +from ...tools.search import google_search_grounding +from ...tools.shell_command import ( # Modified block + check_command_exists_tool, + check_shell_command_safety_tool, + configure_shell_approval_tool, # Added for completeness + configure_shell_whitelist_tool, # Added for completeness + execute_vetted_shell_command_tool, +) +from . import prompt + +documentation_agent = LlmAgent( + model="gemini-2.5-pro-preview-05-06", + name="documentation_agent", + description="Agent specialized in writing and updating documentation", + instruction=prompt.DOCUMENTATION_AGENT_INSTR, + tools=[ + read_file_tool, + list_dir_tool, + edit_file_tool, + codebase_search_tool, + google_search_grounding, + # Shell tools + check_command_exists_tool, # Added + check_shell_command_safety_tool, # Added + configure_shell_approval_tool, # Added + configure_shell_whitelist_tool, # Added + execute_vetted_shell_command_tool, + ], + output_key="documentation", +) + +# Placeholder for actual tool implementation diff --git a/contributing/samples/software_engineer_gemini_mcp/sub_agents/documentation/prompt.py b/contributing/samples/software_engineer_gemini_mcp/sub_agents/documentation/prompt.py new file mode 100644 index 0000000000..2bc1a63c5e --- /dev/null +++ b/contributing/samples/software_engineer_gemini_mcp/sub_agents/documentation/prompt.py @@ -0,0 +1,34 @@ +# ruff: noqa +"""Prompt for the documentation agent.""" + +DOCUMENTATION_AGENT_INSTR = """ +You are an expert Documentation agent. Generate clear, accurate, comprehensive documentation for code, APIs, and projects, following best practices. + +## Core Documentation Workflow: +1. **Identify Scope & Audience:** Determine what to document (function, class, API, project) and for whom (end-users, developers). +2. **Analyze Code & Context:** + * `read_file_content`: Understand code. + * `list_directory_contents`: Grasp project structure. + * `codebase_search`: Find usage, dependencies, purpose. +3. **Research Standards (If Needed):** `google_search_grounding` for doc standards (e.g., Javadoc, OpenAPI), formatting (Markdown), or good examples. +4. **Generate Content:** Write clear, concise, accurate explanations. Include purpose, params, returns, examples, errors, setup. Tailor to audience. Generate docstrings/comments for code, or logical structure for project/API docs (e.g., README.md). +5. **Run Doc Generators (Optional):** If project uses tools (Sphinx, Javadoc), identify command (check `conf.py`, `pom.xml`). Use safe shell workflow to run generator. +6. **Write/Update Doc Files:** Prepare final content (docstrings or full files like README.md). Use `edit_file_content` to create/update files or insert docstrings. Inform user if approval needed. + +## Context: + +{project_context} + + +(Shell command workflow: Use standard safe shell execution: `check_command_exists_tool`, `check_shell_command_safety`, handle approval, `execute_vetted_shell_command`, handle errors.) + +## Reporting Task Progress (Callback to Task Management Agent): +- If your task was delegated with a `task_id` or `task_identifier`, you MUST report progress/completion to `task_management_agent`. +- Use `task_management_agent.receive_task_update` tool with: + - `task_identifier`: Original task ID or identifier. + - `new_status_description`: Clear status string (e.g., "DONE", "COMPLETED: Docs generated", "FAILED: Source not found"). + - `update_message` (optional): Brief details or output summary (e.g., link to docs, updated README path). + - `tasks_file_path` (optional): Path to non-default task markdown file if specified. +- Example: "Call `task_management_agent.receive_task_update` with `task_identifier=\'doc-module-X\'`, `new_status_description=\'DONE - README for Module X updated.\'`, `update_message=\'See /modules/X/README.md\'`." +- Ensure correct `task_identifier`. +""" diff --git a/contributing/samples/software_engineer_gemini_mcp/sub_agents/task_management/__init__.py b/contributing/samples/software_engineer_gemini_mcp/sub_agents/task_management/__init__.py new file mode 100644 index 0000000000..2058f63bcc --- /dev/null +++ b/contributing/samples/software_engineer_gemini_mcp/sub_agents/task_management/__init__.py @@ -0,0 +1 @@ +"""Task Management Sub-Agent.""" diff --git a/contributing/samples/software_engineer_gemini_mcp/sub_agents/task_management/agent.py b/contributing/samples/software_engineer_gemini_mcp/sub_agents/task_management/agent.py new file mode 100644 index 0000000000..487417e19a --- /dev/null +++ b/contributing/samples/software_engineer_gemini_mcp/sub_agents/task_management/agent.py @@ -0,0 +1,261 @@ +""" +Implementation of the Task Management Agent. + +This agent is responsible for: +- Managing a list of tasks (TODO, IN_PROGRESS, DONE, etc.). +- Prioritizing tasks. +- Delegating tasks to appropriate sub-agents. +- Tracking task progress and receiving updates. +""" +import logging +import os +from dotenv import load_dotenv +from typing import Any, Dict, Optional + +from google.adk.agents.llm_agent import LlmAgent +from google.adk.tools import ToolContext, FunctionTool +from google.adk.tools.base_tool import BaseTool +from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset +from google.adk.tools.mcp_tool.mcp_toolset import StdioServerParameters +from google.genai.types import GenerateContentConfig + +# Import task types from shared_libraries +# Assuming 'types.py' is in '.../shared_libraries/' relative to this agent's location +# Adjust path if necessary based on actual project structure when ADK resolves it +# from ...shared_libraries.types import ( +# Task, +# TaskListResponse, +# TaskPriority, +# TaskStatus, +# TaskUpdateRequest, +# ) + +from . import prompt + +# Load .env file +load_dotenv() + +# Placeholder for memory tools - to be refined +# from ....tools.memory_tools import add_memory_fact, search_memory_facts, update_memory_fact + +logger = logging.getLogger(__name__) + +# Default tasks file if none is specified by the user/LLM +DEFAULT_TASKS_MARKDOWN_FILE = "TASKS.md" + +MAX_CONSECUTIVE_PARSING_ERRORS = 5 + +class TaskManagementTools(BaseTool): + """Tools for the Task Management Agent that operate on raw markdown task files, + allowing specification of the target file.""" + + def __init__(self): + super().__init__( + name="TaskManagementTools", + description="Tools for reading and writing the entire content of a user-specified or default markdown task file." + ) + + def _resolve_file_path(self, tasks_file_path: Optional[str] = None) -> str: + """Resolves the file path to be used, defaulting if None is provided.""" + path_to_use = tasks_file_path if tasks_file_path else DEFAULT_TASKS_MARKDOWN_FILE + logger.debug(f"Resolved task file path to: {path_to_use}") + return path_to_use + + def _read_tasks_markdown_file(self, tool_context: ToolContext, file_path: str) -> str: + """Reads the entire content of the specified markdown file.""" + try: + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + logger.info(f"Successfully read {len(content)} characters from '{file_path}'.") + return content + except FileNotFoundError: + logger.info(f"Tasks file '{file_path}' not found. Returning empty string.") + return "" + except IOError as e: + logger.error(f"IOError reading tasks file '{file_path}': {e}. Returning empty string.") + return "" + except Exception as e: + logger.error(f"Unexpected error reading tasks file '{file_path}': {e}. Returning empty string.") + return "" + + def _save_tasks_markdown_file(self, tool_context: ToolContext, file_path: str, markdown_content: str) -> bool: + """Saves the provided markdown content to the specified file, overwriting it. + Returns True on success, False on failure. + """ + try: + with open(file_path, 'w', encoding='utf-8') as f: + f.write(markdown_content) + logger.info(f"Successfully wrote {len(markdown_content)} characters to '{file_path}'.") + return True + except IOError as e: + logger.critical(f"CRITICAL: IOError writing tasks file '{file_path}': {e}. Content NOT saved.") + return False + except Exception as e: + logger.error(f"Unexpected error writing tasks file '{file_path}': {e}. Content NOT saved.") + return False + + def list_all_tasks_markdown(self, tool_context: ToolContext, tasks_file_path: Optional[str] = None) -> str: + """Reads and returns the entire markdown content from a specified task file (default TASKS.md). Use `tasks_file_path` to specify a file.""" + file_to_read = self._resolve_file_path(tasks_file_path) + logger.info(f"Executing list_all_tasks_markdown tool for file: {file_to_read}.") + return self._read_tasks_markdown_file(tool_context, file_to_read) + + def get_specific_task_markdown(self, tool_context: ToolContext, task_identifier: str, tasks_file_path: Optional[str] = None) -> str: + """Reads and returns the entire markdown content from a specified task file (default TASKS.md), used when focusing on a `task_identifier`. Use `tasks_file_path` to specify a file.""" + file_to_read = self._resolve_file_path(tasks_file_path) + logger.info(f"Executing get_specific_task_markdown for identifier: '{task_identifier}' from file: {file_to_read}. Returning all tasks from this file.") + return self._read_tasks_markdown_file(tool_context, file_to_read) + + def save_tasks_markdown( + self, + tool_context: ToolContext, + full_updated_markdown_content: str, + tasks_file_path: Optional[str] = None + ) -> Dict[str, Any]: + """Saves/overwrites the 'full_updated_markdown_content' to a specified task file (default TASKS.md). Use `tasks_file_path` to specify.""" + file_to_write = self._resolve_file_path(tasks_file_path) + logger.info(f"Executing save_tasks_markdown tool for file: {file_to_write} with content length {len(full_updated_markdown_content)}.") + success = self._save_tasks_markdown_file(tool_context, file_to_write, full_updated_markdown_content) + if success: + return {"status": "success", "message": f"Tasks markdown file '{file_to_write}' was successfully updated. Length: {len(full_updated_markdown_content)}."} + else: + return {"status": "error", "message": f"Failed to save tasks markdown file '{file_to_write}' due to a persistence error."} + + def prepare_delegation_and_save_tasks( + self, + tool_context: ToolContext, + task_identifier_for_message: str, + target_agent_name: str, + delegation_instructions_for_message: str, + full_updated_markdown_content: str, + tasks_file_path: Optional[str] = None + ) -> Dict[str, str]: + """Saves 'full_updated_markdown_content' (reflecting a task prepared for delegation) to a task file (default TASKS.md). Use `tasks_file_path`. Returns details for instructing root_agent.""" + file_to_write = self._resolve_file_path(tasks_file_path) + logger.info(f"Executing prepare_delegation_and_save_tasks for task '{task_identifier_for_message}' to agent '{target_agent_name}', saving to file: {file_to_write}.") + + save_success = self._save_tasks_markdown_file(tool_context, file_to_write, full_updated_markdown_content) + + if not save_success: + logger.error(f"Persistence failed for file '{file_to_write}' during delegation preparation for task '{task_identifier_for_message}'.") + return { + "status": "error", + "message": f"Persistence failed for '{file_to_write}' when preparing delegation for task '{task_identifier_for_message}'.", + "task_identifier": task_identifier_for_message, + "target_agent": target_agent_name, + "delegation_instructions": delegation_instructions_for_message + } + + logger.info(f"Task '{task_identifier_for_message}' delegation info saved to '{file_to_write}'. Ready for root_agent instruction.") + return { + "status": "delegation_prepared", + "message": f"Markdown in '{file_to_write}' updated for task '{task_identifier_for_message}' for delegation to {target_agent_name}.", + "task_identifier": task_identifier_for_message, + "target_agent": target_agent_name, + "delegation_instructions": delegation_instructions_for_message + } + + def receive_task_update( + self, + tool_context: ToolContext, + task_identifier: str, + new_status_description: str, + update_message: Optional[str] = None, + tasks_file_path: Optional[str] = None + ) -> Dict[str, str]: + """Receives a task update (identifier, new status, message) from another agent for a task in a specified file (default TASKS.md). Triggers this agent's LLM to process and save the update. Use `tasks_file_path`.""" + file_path_context = tasks_file_path if tasks_file_path else DEFAULT_TASKS_MARKDOWN_FILE + logger.info(f"Executing receive_task_update tool for task_identifier: '{task_identifier}' in file context: '{file_path_context}'. New status: '{new_status_description}', Message: '{update_message}'.") + # Basic validation of inputs + if not task_identifier or not new_status_description: + logger.warning("receive_task_update called with missing task_identifier or new_status_description.") + return { + "status": "error", + "message": "Task identifier and new status description are required.", + "task_identifier": task_identifier or "MISSING" + } + + # The actual work of reading, modifying, and saving the markdown + # will be done by the LLM of this (TaskManagement) agent, guided by its instructions, + # using list_all_tasks_markdown and save_tasks_markdown tools internally. + # This tool call serves as the trigger and data provider for that LLM workflow. + return { + "status": "received", + "message": f"Update for task '{task_identifier}' (new status: '{new_status_description}') received. LLM will process and save to '{file_path_context}'.", + "task_identifier": task_identifier, + "new_status_description": new_status_description, + "update_message": update_message or "", + "tasks_file_path_context": file_path_context # Pass back for LLM context + } + +# Initialize tools +task_tools = TaskManagementTools() + +# Define the default set of tools (custom markdown tools) +default_agent_tools = [ + FunctionTool(task_tools.list_all_tasks_markdown), + FunctionTool(task_tools.get_specific_task_markdown), + FunctionTool(task_tools.save_tasks_markdown), + FunctionTool(task_tools.prepare_delegation_and_save_tasks), + FunctionTool(task_tools.receive_task_update), +] + +mcp_task_tools = [] +agent_tools_to_use = default_agent_tools # Default to custom tools + +try: + # Attempt to initialize MCPToolset + mcp_toolset_instance = MCPToolset( + connection_params=StdioServerParameters( + command="uvx", + args=[ + "mcp-atlassian", + "--confluence-url=" + os.getenv('CONFLUENCE_URL'), + "--confluence-username=" + os.getenv('CONFLUENCE_USERNAME'), + "--confluence-token=" + os.getenv('CONFLUENCE_TOKEN'), + "--jira-url=" + os.getenv('JIRA_URL'), + "--jira-username=" + os.getenv('JIRA_USERNAME'), + "--jira-token=" + os.getenv('JIRA_TOKEN'), + ], + ), + ) + # If MCPToolset loads, add its tools. ADK documentation suggests a Toolset is a list of tools. + # If mcp_toolset_instance is a single tool, wrap it in a list. + # If it's already a list of tools, it can be assigned directly or extended. + # Assuming mcp_toolset_instance itself can be used as the list of tools if it's a ToolSet. + # Or, if MCPToolset is expected to *provide* a list of tools, that method should be called. + # For now, let's assume mcp_toolset_instance can be directly used or is a list of tools. + if mcp_toolset_instance: # Check if it loaded successfully + mcp_task_tools.append(mcp_toolset_instance) # Assuming it's a tool or a list of tools + agent_tools_to_use = mcp_task_tools # Prioritize MCP tools if loaded + logger.info("MCPToolset loaded successfully. Task Management agent will use MCP tools.") + else: + logger.warning( + "MCPToolset initialization did not return a tool instance, though no exception was raised. " + "Falling back to default task management tools." + ) + # agent_tools_to_use remains default_agent_tools +except Exception as e: + logger.warning( + f"Failed to load MCPToolset: {e}. " + "Task Management agent will operate without MCP task management tools, using default custom tools. " + "Custom task management tools remain available." + ) + # agent_tools_to_use remains default_agent_tools + +task_management_agent = LlmAgent( + model="gemini-2.5-pro-preview-05-06", + name="task_management_agent", + description=( + "Manages tasks by reading and writing to a user-specified or default TASKS.md file in markdown format, " + "or via MCP tools if available. " # Updated description + "Can list, create, update, and prepare tasks for delegation." + ), + instruction=prompt.TASK_MANAGEMENT_AGENT_INSTR, # Ensure this prompt guides LLM on which tools might be present + tools=agent_tools_to_use, # Use the conditionally assigned tools + output_key="task_management", + generate_content_config=GenerateContentConfig( + temperature=0.2, + max_output_tokens=8000, + ), +) diff --git a/contributing/samples/software_engineer_gemini_mcp/sub_agents/task_management/prompt.py b/contributing/samples/software_engineer_gemini_mcp/sub_agents/task_management/prompt.py new file mode 100644 index 0000000000..bda90d1a39 --- /dev/null +++ b/contributing/samples/software_engineer_gemini_mcp/sub_agents/task_management/prompt.py @@ -0,0 +1,66 @@ +"""Prompt for the task management agent.""" + +TASK_MANAGEMENT_AGENT_INSTR = """ +You are a Task Management agent. Your primary responsibility is to manage a list of tasks. +**Your available tools will determine how you interact with tasks.** +- **If MCP (e.g., Jira/Confluence) tools are available in your tool list:** You should PRIORITIZE using these tools for all task creation, updating, listing, and status tracking. These tools interact with a central task management system. +- **If MCP tools are NOT available:** You will manage tasks in a markdown file. By default, this is `TASKS.md` in the workspace root. The user can specify a different markdown file path (e.g., "@IMPROVEMENTS.md", "project_alpha/TODO.md"), which you should use. When using markdown, you'll read the entire file and provide complete updated content for changes. + +**Core Responsibilities (adapt based on available tools):** + +1. **Listing Tasks:** + * MCP: Use the relevant MCP tool to query and list tasks based on user criteria. + * Markdown: Use `list_all_tasks_markdown` (specifying `tasks_file_path` if not default) to get current content. + +2. **Creating a Task:** + * MCP: Use the MCP tool to create a new task in the system (e.g., create Jira issue). + * Markdown: + 1. Call `list_all_tasks_markdown` to get current content. + 2. Formulate the new task's markdown. + 3. Append/integrate into existing markdown. + 4. Call `save_tasks_markdown` with the *entire new markdown content* and `tasks_file_path`. + +3. **Updating a Task:** + * MCP: Use the MCP tool to find and update the task (e.g., update Jira issue status, description). + * Markdown: + 1. Call `list_all_tasks_markdown` for current content. + 2. Identify task in markdown. + 3. Formulate changes. + 4. Call `save_tasks_markdown` with *entire new markdown content* and `tasks_file_path`. + +4. **Delegating a Task:** + * Identify the task and the target agent. + * **Update Task Status for Delegation:** + * MCP: Update the task in the MCP system (e.g., Jira) to note it's "Pending Delegation" or assign it conceptually. + * Markdown: Update the task's markdown to reflect delegation details. + * **Saving Changes (if applicable to the toolset):** + * MCP: The act of updating the task in the MCP system is the save. + * Markdown: If you modified the markdown, you must save it. The `prepare_delegation_and_save_tasks` tool is designed for this markdown workflow. It takes `full_updated_markdown_content`. + * **Formulate Root Agent Directive:** Regardless of MCP/Markdown, your final step is to instruct the `root_agent`. + 1. Prepare `task_identifier_for_message`, `target_agent_name`, `delegation_instructions_for_message`. + 2. If using markdown tools, the `prepare_delegation_and_save_tasks` tool returns these details after saving. + 3. If using MCP tools, you'll have these details from your interaction with the MCP system and user request. + 4. Your final textual response MUST be: "@root_agent, please transfer to [target_agent_name] with the following instructions: '[delegation_instructions_for_message]'. The task identifier is '[task_identifier_for_message]'." + 5. Your final action for this turn MUST be to call/invoke the `root_agent` with this directive. + +5. **Receiving Task Updates (via `receive_task_update` tool call):** + * This tool is called with `task_identifier`, `new_status_description`, optional `update_message`, and `tasks_file_path` (for markdown context). + * **Your responsibility is to:** + * MCP: Use the MCP tools to find the task by `task_identifier` and update its status/details with `new_status_description` and `update_message`. + * Markdown: + a. Call `list_all_tasks_markdown` (using `tasks_file_path` from the tool call) for current content. + b. Locate task in markdown using `task_identifier`. + c. Modify its markdown for `new_status_description` and `update_message`. + d. Call `save_tasks_markdown` with the full modified content and `tasks_file_path`. + * The `receive_task_update` tool itself mainly serves as a trigger and data container; your LLM logic does the actual update using the appropriate toolset. + +**Specific Delegation Scenarios (adapt instructions for target agent):** +- **File Deletion**: Delegate to `devops_agent`. E.g., "Please delete file 'X'. Task: 'delete file X'. Report completion." +- **Shell Commands**: Delegate to `devops_agent`. E.g., "Execute shell command 'Y'. Task: 'run command Y'. Report output/status." +- Others: Delegate to `documentation_agent`, `testing_agent` etc., with clear instructions and task identifier. + +**General Guidelines:** +- **Tool Awareness:** Check your tool descriptions. If MCP tools are present, use them. If not, use markdown tools (`list_all_tasks_markdown`, `save_tasks_markdown`, `get_specific_task_markdown`, `prepare_delegation_and_save_tasks`). +- **Markdown Context:** If using markdown tools, always confirm/infer which task file to use (`tasks_file_path` or default `TASKS.md`). +- **Task Identifiers:** Ensure clear task identifiers for lookups and callbacks, whether it's an MCP ID or a unique markdown description. +""" diff --git a/contributing/samples/software_engineer_gemini_mcp/sub_agents/testing/__init__.py b/contributing/samples/software_engineer_gemini_mcp/sub_agents/testing/__init__.py new file mode 100644 index 0000000000..ecf12adc05 --- /dev/null +++ b/contributing/samples/software_engineer_gemini_mcp/sub_agents/testing/__init__.py @@ -0,0 +1 @@ +"""Testing agent for generating test cases and testing strategies.""" diff --git a/contributing/samples/software_engineer_gemini_mcp/sub_agents/testing/agent.py b/contributing/samples/software_engineer_gemini_mcp/sub_agents/testing/agent.py new file mode 100644 index 0000000000..14a06c0047 --- /dev/null +++ b/contributing/samples/software_engineer_gemini_mcp/sub_agents/testing/agent.py @@ -0,0 +1,39 @@ +"""Testing Agent Implementation.""" + +from google.adk.agents import LlmAgent + +# Import tools from the parent 'tools' module +from ...tools import codebase_search_tool +from ...tools.filesystem import edit_file_tool, list_dir_tool, read_file_tool +from ...tools.search import google_search_grounding +from ...tools.shell_command import ( # Modified block + check_command_exists_tool, + check_shell_command_safety_tool, + configure_shell_approval_tool, + configure_shell_whitelist_tool, + execute_vetted_shell_command_tool, +) +from . import prompt + +testing_agent = LlmAgent( + model="gemini-1.5-pro-001", + name="testing_agent", + description="Agent specialized in writing and running tests", + instruction=prompt.TESTING_AGENT_INSTR, + tools=[ + read_file_tool, + list_dir_tool, + edit_file_tool, + codebase_search_tool, + google_search_grounding, + # Shell tools + check_command_exists_tool, # Added + check_shell_command_safety_tool, # Added + configure_shell_approval_tool, # Added + configure_shell_whitelist_tool, # Added + execute_vetted_shell_command_tool, + ], + output_key="testing", +) + +# Placeholder for actual tool implementation diff --git a/contributing/samples/software_engineer_gemini_mcp/sub_agents/testing/prompt.py b/contributing/samples/software_engineer_gemini_mcp/sub_agents/testing/prompt.py new file mode 100644 index 0000000000..adf771c7cc --- /dev/null +++ b/contributing/samples/software_engineer_gemini_mcp/sub_agents/testing/prompt.py @@ -0,0 +1,37 @@ +# ruff: noqa +"""Prompt for the testing agent.""" + +TESTING_AGENT_INSTR = """ +You are a diligent Testing agent. Help developers create comprehensive, effective automated tests (unit, integration) to ensure reliability and maintainability. Generate test cases, explain strategies, suggest improvements, and aim to improve coverage. + +## Core Responsibilities: +1. **Tool Discovery:** Identify project's testing framework & execution command. + * Check project config (`pyproject.toml`, `package.json`, `Makefile`) for test scripts/dependencies. + * Based on language, look for common runners (Python: `pytest`; JS: `jest`; Java: `JUnit`; Go: `go test`). Adapt as needed. + * Verify availability of test command (e.g., `pytest`) and coverage tools (e.g., `coverage`) using `check_command_exists_tool`. Report discovered tools. +2. **Understand Code:** + * `read_file_content`: Fetch source code to test. + * `list_directory_contents`: Understand project structure, locate/determine test file location. + * `codebase_search`: Understand functionality, dependencies, usage patterns. +3. **Generate Tests:** Write clear, readable, maintainable tests for public interfaces. Include happy paths, edge cases, error handling. Use mocking/stubs. Follow best practices. Output: complete content for new/modified test file(s) for `edit_file_content`. +4. **Write Test Files:** Use `edit_file_content` to create/add tests in appropriate directory. Inform user if approval needed. +5. **Run Tests & Coverage (Optional):** Execute discovered test command (and coverage tool if available) using safe shell workflow. Analyze results. If tests fail, attempt to debug. +6. **Output Format:** Present findings, strategies, test cases in **markdown**. Use code blocks for test code. + +## Reporting Task Progress (Callback to Task Management Agent): +- If your task was delegated with a `task_id` or `task_identifier`, you MUST report progress/completion to `task_management_agent`. +- Use `task_management_agent.receive_task_update` tool with: + - `task_identifier`: Original task ID or identifier. + - `new_status_description`: Clear status string (e.g., "DONE", "COMPLETED: Tests written & passing", "FAILED: Cannot run tests"). + - `update_message` (optional): Brief details or output summary (e.g., test results, num tests, test file path). + - `tasks_file_path` (optional): Path to non-default task markdown file if specified. +- Example: "Call `task_management_agent.receive_task_update` with `task_identifier=\'test-module-Y\'`, `new_status_description=\'DONE - 15 tests generated, all passing.\'`, `update_message=\'Tests in /tests/test_Y.py. Coverage 85%\'`." +- Ensure correct `task_identifier`. + +## Context: + +{project_context} + + +(Shell command workflow: Use standard safe shell execution: `check_command_exists_tool`, `check_shell_command_safety`, handle approval, `execute_vetted_shell_command`, handle errors.) +""" diff --git a/contributing/samples/software_engineer_gemini_mcp/tools/__init__.py b/contributing/samples/software_engineer_gemini_mcp/tools/__init__.py new file mode 100644 index 0000000000..e44d43dcf4 --- /dev/null +++ b/contributing/samples/software_engineer_gemini_mcp/tools/__init__.py @@ -0,0 +1,78 @@ +# ruff: noqa: I001, F401 +"""Tools for the Software Engineer Multi-Agent.""" + +from . import ( + analysis_state, + code_analysis, + code_search, + filesystem, + search, + shell_command, + system_info, +) + +# Export code analysis tools +from .code_analysis import ( + analyze_code_tool, + get_analysis_issues_by_severity_tool, + suggest_code_fixes_tool, +) + +# Export the code search tool for easier imports +from .code_search import codebase_search_tool + +# Export filesystem tools +from .filesystem import ( + read_file_tool, + list_dir_tool, + edit_file_tool, + configure_approval_tool, +) + +# Export shell command tools +from .shell_command import ( + check_command_exists_tool, + check_shell_command_safety_tool, + configure_shell_approval_tool, + configure_shell_whitelist_tool, + execute_vetted_shell_command_tool, +) + +# Export search tools +from .search import google_search_grounding + +from .system_info import get_os_info_tool + +# Import system info tools + +# Import the placeholder memory persistence tools +from .persistent_memory_tool import ( + save_current_session_to_file_tool, + load_memory_from_file_tool, +) + +__all__ = [ + # Filesystem Tools + "read_file_tool", + "list_dir_tool", + "edit_file_tool", + "configure_approval_tool", + # Shell Command Tools + "check_command_exists_tool", + "check_shell_command_safety_tool", + "configure_shell_approval_tool", + "configure_shell_whitelist_tool", + "execute_vetted_shell_command_tool", + # Code Analysis Tools (add if needed by root agent, or keep in sub-agent) + # "analyze_code_tool", + # "get_analysis_issues_by_severity_tool", + # "suggest_code_fixes_tool", + # Search Tools + "google_search_grounding", + "codebase_search_tool", + # System Info Tools + "get_os_info_tool", + # Placeholder Persistent Memory Tools + "save_current_session_to_file_tool", + "load_memory_from_file_tool", +] diff --git a/contributing/samples/software_engineer_gemini_mcp/tools/analysis_state.py b/contributing/samples/software_engineer_gemini_mcp/tools/analysis_state.py new file mode 100644 index 0000000000..e1e99e264c --- /dev/null +++ b/contributing/samples/software_engineer_gemini_mcp/tools/analysis_state.py @@ -0,0 +1,37 @@ +# software_engineer/tools/analysis_state.py +"""Tool to access analysis results stored in session state.""" + +# TODO: This tool reads from tool_context.state['analysis_issues']. +# It requires another tool or agent (e.g., an enhanced analyze_code_tool or +# the code_review agent after running linters) to populate this state key +# with structured issue data (e.g., list of dicts) for this tool to be useful. + +from typing import Any, Dict + +from google.adk.tools import FunctionTool, ToolContext + + +def get_analysis_issues(tool_context: ToolContext) -> Dict[str, Any]: + """ + Retrieves code analysis issues stored under the key 'analysis_issues' in the session state. + + Args: + tool_context: The tool context from ADK, providing access to session state. + + Returns: + A dictionary containing the list of analysis issues under the key 'issues', + or an empty list if the 'analysis_issues' key is not found or not a list. + """ + # Get the issues list, default to empty list if key not found or value is None + issues = tool_context.state.get("analysis_issues") + # Ensure it's a list, return empty list otherwise + if not isinstance(issues, list): + issues = [] + return {"issues": issues} + + +# Define the tool using FunctionTool +get_analysis_issues_tool = FunctionTool( + func=get_analysis_issues, + # Description comes from func docstring +) diff --git a/contributing/samples/software_engineer_gemini_mcp/tools/code_analysis.py b/contributing/samples/software_engineer_gemini_mcp/tools/code_analysis.py new file mode 100644 index 0000000000..6a66621ffd --- /dev/null +++ b/contributing/samples/software_engineer_gemini_mcp/tools/code_analysis.py @@ -0,0 +1,419 @@ +"""Code analysis tool for the software engineer agent. + +This tool performs static analysis on code files using language-specific analyzers +and provides detailed reports on quality, complexity, and potential issues. +""" + +import os +import re +from enum import Enum +from typing import Any, Dict, List, Optional + +from google.adk.tools import FunctionTool, ToolContext +from pydantic import BaseModel, Field + +# Third-party analysis libraries - using try/except to make dependencies optional +try: + import pylint.lint + import pylint.reporters.text + + PYLINT_AVAILABLE = True +except ImportError: + PYLINT_AVAILABLE = False + +try: + # NOTE: flake8.api.legacy is no longer available in newer versions of flake8 + # We'll use a different approach for flake8 + import flake8 # noqa: F401 + from flake8.main.application import Application + + FLAKE8_AVAILABLE = True +except ImportError: + FLAKE8_AVAILABLE = False + +try: + import radon.complexity + import radon.metrics + + RADON_AVAILABLE = True +except ImportError: + RADON_AVAILABLE = False + +try: + import bandit + from bandit.core import manager as bandit_manager + + BANDIT_AVAILABLE = True +except ImportError: + BANDIT_AVAILABLE = False + + +class AnalysisSeverity(str, Enum): + """Severity levels for analysis issues.""" + + CRITICAL = "critical" + ERROR = "error" + WARNING = "warning" + INFO = "info" + + +class CodeIssue(BaseModel): + """Model for code issues identified during analysis.""" + + line: Optional[int] = None + column: Optional[int] = None + severity: AnalysisSeverity = AnalysisSeverity.INFO + message: str + code: Optional[str] = None + source: str # The tool that found this issue (e.g., "pylint", "flake8") + + +class AnalyzeCodeInput(BaseModel): + """Input model for code analysis.""" + + file_path: str = Field(description="Path to the file to analyze") + + +class CodeAnalysisResult(BaseModel): + """Result model for code analysis.""" + + file_path: str + language: str + lines_of_code: int + issues: List[CodeIssue] = [] + metrics: Dict[str, Any] = {} + status: str + error: Optional[str] = None + + +def detect_language(file_path: str) -> str: + """ + Detect the programming language from a file extension. + + Args: + file_path: Path to the file + + Returns: + String representing the language + """ + ext = os.path.splitext(file_path)[1].lower() + language_map = { + ".py": "python", + ".js": "javascript", + ".ts": "typescript", + ".java": "java", + ".c": "c", + ".cpp": "cpp", + ".h": "c_header", + ".hpp": "cpp_header", + ".go": "go", + ".rb": "ruby", + ".php": "php", + ".cs": "csharp", + ".rs": "rust", + ".swift": "swift", + ".kt": "kotlin", + ".sh": "shell", + } + return language_map.get(ext, "unknown") + + +def analyze_python_code(file_path: str, code: str) -> Dict[str, Any]: + """ + Analyze Python code using pylint, flake8, radon, and bandit. + + Args: + file_path: Path to the Python file + code: Content of the file + + Returns: + Dict with analysis results + """ + issues = [] + metrics = {} + + # Run pylint + if PYLINT_AVAILABLE: + try: + from io import StringIO + + output = StringIO() + reporter = pylint.reporters.text.TextReporter(output) + + pylint.lint.Run([file_path, "--output-format=text"], reporter=reporter, exit=False) + + pylint_output = output.getvalue() + # TODO: Sonar Report - https://sonarcloud.io/project/security_hotspots?id=BlueCentre_code-agent&pullRequest=19&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true + # NOTE: Make sure the regex used here, which is vulnerable to polynomial runtime due to backtracking, cannot lead to denial of service. + # Parse pylint output + pattern = r"([A-Z]):\s*(\d+),\s*(\d+):\s*(.+)\s*\(([A-Z0-9]+)\)" + for match in re.finditer(pattern, pylint_output): + severity_code, line, col, message, code = match.groups() + severity_map = { + "E": AnalysisSeverity.ERROR, + "F": AnalysisSeverity.CRITICAL, + "W": AnalysisSeverity.WARNING, + "C": AnalysisSeverity.INFO, + "R": AnalysisSeverity.INFO, + } + + issues.append( + CodeIssue( + line=int(line), + column=int(col), + severity=severity_map.get(severity_code, AnalysisSeverity.INFO), + message=message.strip(), + code=code, + source="pylint", + ) + ) + except Exception as e: + issues.append(CodeIssue(severity=AnalysisSeverity.ERROR, message=f"Error running pylint: {e!s}", source="analyzer")) + + # Run flake8 + if FLAKE8_AVAILABLE: + try: + # Use the Application class directly instead of the legacy API + flake8_app = Application() + flake8_app.initialize([file_path]) + flake8_app.run_checks([file_path]) + flake8_app.formatter.start() + + for file_errors in flake8_app.guide.stats.statistics_for(""): + for error in file_errors: + if len(error) >= 4: # Make sure the error has all components + line_num, col_num, message = error[0], error[1], error[2] + # line_num, col_num, message, code_obj = error[0], error[1], error[2], error[3] + + severity = AnalysisSeverity.WARNING + if message.startswith("E"): + severity = AnalysisSeverity.ERROR + elif message.startswith("F"): + severity = AnalysisSeverity.CRITICAL + + issues.append(CodeIssue(line=line_num, column=col_num, severity=severity, message=message, code=message.split(" ")[0], source="flake8")) + + flake8_app.formatter.stop() + flake8_app.report_errors() + except Exception as e: + issues.append(CodeIssue(severity=AnalysisSeverity.ERROR, message=f"Error running flake8: {e!s}", source="analyzer")) + + # Run radon for complexity metrics + if RADON_AVAILABLE: + try: + # Cyclomatic Complexity + cc_blocks = radon.complexity.cc_visit(code) + avg_complexity = sum(block.complexity for block in cc_blocks) / len(cc_blocks) if cc_blocks else 0 + + # Maintainability Index + mi_score = radon.metrics.mi_visit(code, multi=True) + + metrics["cyclomatic_complexity"] = { + "average": avg_complexity, + "blocks": [{"name": block.name, "complexity": block.complexity, "rank": block.rank, "line": block.lineno} for block in cc_blocks], + } + + metrics["maintainability_index"] = mi_score + + # Flag high complexity functions + for block in cc_blocks: + if block.complexity > 10: + severity = AnalysisSeverity.WARNING + if block.complexity > 20: + severity = AnalysisSeverity.ERROR + if block.complexity > 30: + severity = AnalysisSeverity.CRITICAL + + issues.append( + CodeIssue( + line=block.lineno, + severity=severity, + message=f"High cyclomatic complexity ({block.complexity}) in {block.name}", + code="R001", + source="radon", + ) + ) + except Exception as e: + issues.append(CodeIssue(severity=AnalysisSeverity.ERROR, message=f"Error calculating code complexity: {e!s}", source="analyzer")) + + # Run bandit for security analysis + if BANDIT_AVAILABLE: + try: + mgr = bandit_manager.BanditManager() + mgr.discover_files([file_path]) + mgr.run_tests() + + for issue in mgr.get_issue_list(): + severity_map = { + bandit.constants.HIGH: AnalysisSeverity.CRITICAL, + bandit.constants.MEDIUM: AnalysisSeverity.ERROR, + bandit.constants.LOW: AnalysisSeverity.WARNING, + } + + issues.append( + CodeIssue( + line=issue.lineno, + severity=severity_map.get(issue.severity, AnalysisSeverity.INFO), + message=issue.text, + code=issue.test_id, + source="bandit", + ) + ) + except Exception as e: + issues.append(CodeIssue(severity=AnalysisSeverity.ERROR, message=f"Error running security analysis: {e!s}", source="analyzer")) + + return {"issues": issues, "metrics": metrics} + + +def analyze_javascript_code(file_path: str, code: str) -> Dict[str, Any]: + """ + Analyze JavaScript code. + + Currently a placeholder for JavaScript analysis. + + Args: + file_path: Path to the JavaScript file + code: Content of the file + + Returns: + Dict with analysis results + """ + # Placeholder for JavaScript analysis + # In a real implementation, we would integrate tools like ESLint + return {"issues": [CodeIssue(severity=AnalysisSeverity.INFO, message="JavaScript analysis not yet implemented", source="analyzer")], "metrics": {}} + + +def _analyze_code(file_path: str, tool_context: ToolContext) -> Dict[str, Any]: + """ + Analyze code in a file for quality issues. + + Args: + file_path: Path to the file to analyze. + tool_context: The tool context from ADK. + + Returns: + Dict containing analysis results. + """ + try: + if not os.path.exists(file_path): + return {"error": f"File {file_path} does not exist", "status": "Failed"} + + with open(file_path, "r", encoding="utf-8") as file: + code = file.read() + + # Store the code in the state for the agent to access + tool_context.state["analyzed_code"] = code + tool_context.state["analyzed_file"] = file_path + + # Detect language + language = detect_language(file_path) + + # Initialize result object + result = CodeAnalysisResult(file_path=file_path, language=language, lines_of_code=len(code.split("\n")), status="Analysis complete") + + # Analyze based on language + if language == "python": + analysis = analyze_python_code(file_path, code) + result.issues = analysis["issues"] + result.metrics = analysis["metrics"] + elif language in ["javascript", "typescript"]: + analysis = analyze_javascript_code(file_path, code) + result.issues = analysis["issues"] + result.metrics = analysis["metrics"] + else: + result.issues.append(CodeIssue(severity=AnalysisSeverity.INFO, message=f"Analysis for {language} is not yet supported", source="analyzer")) + + # Store analysis issues in state for other tools to use + tool_context.state["analysis_issues"] = [issue.dict() for issue in result.issues] + + # Generate summary statistics + issue_counts = { + "critical": sum(1 for i in result.issues if i.severity == AnalysisSeverity.CRITICAL), + "error": sum(1 for i in result.issues if i.severity == AnalysisSeverity.ERROR), + "warning": sum(1 for i in result.issues if i.severity == AnalysisSeverity.WARNING), + "info": sum(1 for i in result.issues if i.severity == AnalysisSeverity.INFO), + } + result.metrics["issue_summary"] = issue_counts + + return result.dict() + except Exception as e: + return {"error": f"Error analyzing file: {e!s}", "status": "Failed"} + + +# Define the tool using FunctionTool +analyze_code_tool = FunctionTool(func=_analyze_code) + + +def get_issues_by_severity(tool_context: ToolContext, severity: Optional[str] = None) -> Dict[str, Any]: + """ + Retrieves code analysis issues filtered by severity. + + Args: + tool_context: The tool context from ADK. + severity: Optional severity level to filter by (critical, error, warning, info) + + Returns: + Dict containing filtered issues list. + """ + # Get all issues + issues = tool_context.state.get("analysis_issues", []) + + # If no severity specified, return all issues + if not severity: + return {"issues": issues} + + # Filter by severity + filtered_issues = [issue for issue in issues if issue.get("severity", "").lower() == severity.lower()] + + return {"issues": filtered_issues, "count": len(filtered_issues), "total_issues": len(issues), "severity": severity} + + +# Define additional tools to work with analysis results +get_analysis_issues_by_severity_tool = FunctionTool(func=get_issues_by_severity) + + +def suggest_fixes(tool_context: ToolContext) -> Dict[str, Any]: + """ + Analyzes issues and suggests fixes based on common patterns. + + Args: + tool_context: The tool context from ADK. + + Returns: + Dict containing suggested fixes for detected issues. + """ + issues = tool_context.state.get("analysis_issues", []) + + suggested_fixes = [] + + # Common patterns and their fixes + for issue in issues: + code = issue.get("code", "") + message = issue.get("message", "") + suggestion = None + + # Unused import suggestions + if "unused import" in message.lower(): + suggestion = f"Remove the unused import at line {issue.get('line')}" + + # Undefined variable suggestions + elif "undefined variable" in message.lower(): + var_name = message.split("'")[1] if "'" in message else "" + suggestion = f"Define variable '{var_name}' before use or check for typo" + + # Line too long + elif code == "E501" or "line too long" in message.lower(): + suggestion = f"Break the long line at {issue.get('line')} into multiple lines" + + # Complexity suggestions + elif "complexity" in message.lower(): + suggestion = f"Refactor the complex function at line {issue.get('line')} into smaller functions" + + if suggestion: + suggested_fixes.append({"issue": issue, "suggestion": suggestion}) + + return {"suggested_fixes": suggested_fixes, "count": len(suggested_fixes), "analyzed_file": tool_context.state.get("analyzed_file")} + + +# Define a tool for suggesting fixes +suggest_code_fixes_tool = FunctionTool(func=suggest_fixes) diff --git a/contributing/samples/software_engineer_gemini_mcp/tools/code_search.py b/contributing/samples/software_engineer_gemini_mcp/tools/code_search.py new file mode 100644 index 0000000000..36b1db1b71 --- /dev/null +++ b/contributing/samples/software_engineer_gemini_mcp/tools/code_search.py @@ -0,0 +1,78 @@ +"""Code search functionality for software engineer agents using ripgrep.""" + +import json +import subprocess +from typing import Any, Dict, List, Optional + +from google.adk.tools import FunctionTool + + +def ripgrep_code_search(query: str, target_directories: Optional[List[str]] = None, explanation: Optional[str] = None) -> Dict[str, Any]: + """ + Perform a code search using ripgrep (rg) and return the results. + + Args: + query: The search query to find relevant code + target_directories: Optional list of directories to search in (glob patterns supported) + explanation: Optional explanation of why this search is being performed + + Returns: + Dictionary containing search results with snippets and file information + """ + try: + # Default to search in current directory if none specified + search_paths = target_directories or ["."] + + results = [] + for path in search_paths: + # Build the ripgrep command + # Using --json for structured output + # --context for showing surrounding lines + cmd = [ + "rg", + "--json", + "--context", + "2", # Show 2 lines before and after matches + "--max-columns", + "1000", # Reasonable line length limit + query, + ] + + # Add path to search + cmd.append(path) + + # Execute the search + process = subprocess.run( + cmd, + capture_output=True, + text=True, + check=False, # Don't raise exception if nothing found + ) + + # Process the output - each line is a JSON object + for line in process.stdout.strip().split("\n"): + if not line: + continue + + try: + data = json.loads(line) + + # Only process match data + if data.get("type") == "match": + file_path = data.get("data", {}).get("path", {}).get("text", "") + line_number = data.get("data", {}).get("line_number", 0) + match_content = data.get("data", {}).get("lines", {}).get("text", "").strip() + + results.append({"file": file_path, "line": line_number, "content": match_content}) + except json.JSONDecodeError: + # Skip lines that aren't valid JSON + continue + + return {"snippets": results, "status": "success", "query": query, "explanation": explanation or "Code search results"} + + except Exception as e: + return {"snippets": [], "status": "error", "error_message": str(e), "query": query} + + +# Create FunctionTool wrapper for ripgrep code search +codebase_search_tool = FunctionTool(func=ripgrep_code_search) diff --git a/contributing/samples/software_engineer_gemini_mcp/tools/filesystem.py b/contributing/samples/software_engineer_gemini_mcp/tools/filesystem.py new file mode 100644 index 0000000000..f926a5568d --- /dev/null +++ b/contributing/samples/software_engineer_gemini_mcp/tools/filesystem.py @@ -0,0 +1,194 @@ +# code_agent/agent/software_engineer/software_engineer/tools/filesystem_tools.py +import logging +import os +from typing import Any, Dict + +from google.adk.tools import FunctionTool, ToolContext + +logger = logging.getLogger(__name__) + +# Consider adding a WORKSPACE_ROOT validation here for security +# WORKSPACE_ROOT = os.path.abspath(".") # Example: Use current working directory + + +def read_file_content(filepath: str) -> Dict[str, Any]: + """ + Reads the content of a file from the local filesystem. + + Args: + filepath: The relative or absolute path to the file. + Relative paths are resolved from the agent's current working directory. + (Security Note: Path validation should be implemented to restrict access). + + Returns: + A dictionary with: + - {'status': 'success', 'content': 'file_content_string'} on success. + - {'status': 'error', 'error_type': str, 'message': str} on failure. + Possible error_types: 'FileNotFound', 'PermissionDenied', 'IOError', 'SecurityViolation' (if implemented). + """ + logger.info(f"Attempting to read file: {filepath}") + # Add path validation/sandboxing here before opening + # Example: + # abs_path = os.path.abspath(filepath) + # if not abs_path.startswith(WORKSPACE_ROOT): + # message = f"Access denied: Path '{filepath}' is outside the allowed workspace." + # logger.error(message) + # return {"status": "error", "error_type": "SecurityViolation", "message": message} + try: + with open(filepath, "r", encoding="utf-8") as f: + content = f.read() + logger.info(f"Successfully read file: {filepath}") + return {"status": "success", "content": content} + except FileNotFoundError: + message = f"File not found at path '{filepath}'." + logger.error(message) + return {"status": "error", "error_type": "FileNotFound", "message": message} + except PermissionError: + message = f"Permission denied when trying to read file '{filepath}'." + logger.error(message) + return {"status": "error", "error_type": "PermissionDenied", "message": message} + except Exception as e: + message = f"An unexpected error occurred while reading file '{filepath}': {e}" + logger.error(message, exc_info=True) + return {"status": "error", "error_type": "IOError", "message": message} + + +def list_directory_contents(directory_path: str) -> Dict[str, Any]: + """ + Lists the contents (files and directories) of a directory on the local filesystem. + + Args: + directory_path: The relative or absolute path to the directory. + Relative paths are resolved from the agent's current working directory. + (Security Note: Path validation should be implemented to restrict access). + + Returns: + A dictionary with: + - {'status': 'success', 'contents': ['item1', 'item2', ...]} on success. + - {'status': 'error', 'error_type': str, 'message': str} on failure. + Possible error_types: 'NotADirectory', 'FileNotFound', 'PermissionDenied', 'IOError', 'SecurityViolation' (if implemented). + """ + logger.info(f"Attempting to list directory: {directory_path}") + # Add path validation/sandboxing here + # Example: + # abs_path = os.path.abspath(directory_path) + # if not abs_path.startswith(WORKSPACE_ROOT): + # message = f"Access denied: Path '{directory_path}' is outside the allowed workspace." + # logger.error(message) + # return {"status": "error", "error_type": "SecurityViolation", "message": message} + try: + if not os.path.isdir(directory_path): + message = f"The specified path '{directory_path}' is not a valid directory." + logger.warning(message) + return {"status": "error", "error_type": "NotADirectory", "message": message} + contents = os.listdir(directory_path) + logger.info(f"Successfully listed directory: {directory_path}") + return {"status": "success", "contents": contents} + except FileNotFoundError: + message = f"Directory not found at path '{directory_path}'." + logger.error(message) + return {"status": "error", "error_type": "FileNotFound", "message": message} + except PermissionError: + message = f"Permission denied when trying to list directory '{directory_path}'." + logger.error(message) + return {"status": "error", "error_type": "PermissionDenied", "message": message} + except Exception as e: + message = f"An unexpected error occurred while listing directory '{directory_path}': {e}" + logger.error(message, exc_info=True) + return {"status": "error", "error_type": "IOError", "message": message} + + +def edit_file_content(filepath: str, content: str, tool_context: ToolContext) -> Dict[str, Any]: + """ + Writes content to a file or proposes the write, requiring user approval based on session state. + Creates the file if it does not exist (including parent directories). + Overwrites the file if it already exists (only if approval is not required or already granted). + + Checks the 'require_edit_approval' flag in session state (defaults to True). + If True, returns a 'pending_approval' status without writing. + If False, writes the file and returns 'success' or 'error'. + + Args: + filepath: The relative or absolute path to the file. + Relative paths are resolved from the agent's current working directory. + (Security Note: Path validation should be implemented to restrict access). + content: The new content to write to the file. + + Returns: + A dictionary with: + - {'status': 'pending_approval', 'proposed_filepath': str, 'proposed_content': str, 'message': str} if approval is required. + - {'status': 'success', 'message': 'Success message'} on successful write (when approval not required). + - {'status': 'error', 'error_type': str, 'message': str} on failure during write or validation. + Possible error_types: 'PermissionDenied', 'IOError', 'SecurityViolation' (if implemented). + """ + logger.info(f"Checking approval requirement for writing to file: {filepath}") + + # Add path validation/sandboxing here FIRST + # Example: + # abs_path = os.path.abspath(filepath) + # if not abs_path.startswith(WORKSPACE_ROOT): + # message = f"Access denied: Path '{filepath}' is outside the allowed workspace." + # logger.error(message) + # return {"status": "error", "error_type": "SecurityViolation", "message": message} + + # TODO: Remove this once we have a proper approval mechanism + needs_approval = tool_context.state.get("require_edit_approval", False) # MODIFIED LINE: Default to False + + if needs_approval: + logger.info(f"Approval required for file edit: {filepath}. Returning pending status.") + return { + "status": "pending_approval", + "proposed_filepath": filepath, + "proposed_content": content, + "message": f"Approval required to write to '{filepath}'. User confirmation needed.", + } + + # Proceed with write only if approval is not required + logger.info(f"Approval not required. Proceeding with write to file: {filepath}") + try: + # Ensure the directory exists + dir_path = os.path.dirname(filepath) + if dir_path: # Ensure dir_path is not empty (happens for root-level files) + os.makedirs(dir_path, exist_ok=True) # Creates parent dirs if needed + + # Consider atomic write here: write to temp file, then os.replace() + with open(filepath, "w", encoding="utf-8") as f: + f.write(content) + message = f"Successfully wrote content to '{filepath}'." + logger.info(message) + return {"status": "success", "message": message} + except PermissionError: + message = f"Permission denied when trying to write to file '{filepath}'." + logger.error(message) + return {"status": "error", "error_type": "PermissionDenied", "message": message} + except Exception as e: + message = f"An unexpected error occurred while writing to file '{filepath}': {e}" + logger.error(message, exc_info=True) + return {"status": "error", "error_type": "IOError", "message": message} + + +def configure_edit_approval(require_approval: bool, tool_context: ToolContext) -> Dict[str, Any]: + """ + Configures whether file edits require user approval for the current session. + Sets the 'require_edit_approval' flag in the session state. + + Args: + require_approval: Set to True to require approval (default), False to allow direct edits. + + Returns: + A dictionary confirming the setting change: + - {'status': 'success', 'message': 'Confirmation message'} + """ + logger.info(f"Setting 'require_edit_approval' state to: {require_approval}") + tool_context.state["require_edit_approval"] = require_approval + message = f"File edit approval requirement set to: {require_approval} for this session." + logger.info(message) + return {"status": "success", "message": message} + + +# Wrap functions with FunctionTool +# Note: The return type for the tool schema remains the base function's return type hint (Dict[str, Any]) +read_file_tool = FunctionTool(read_file_content) +list_dir_tool = FunctionTool(list_directory_contents) +edit_file_tool = FunctionTool(edit_file_content) +configure_approval_tool = FunctionTool(configure_edit_approval) diff --git a/contributing/samples/software_engineer_gemini_mcp/tools/memory_tools.py b/contributing/samples/software_engineer_gemini_mcp/tools/memory_tools.py new file mode 100644 index 0000000000..5193dcc198 --- /dev/null +++ b/contributing/samples/software_engineer_gemini_mcp/tools/memory_tools.py @@ -0,0 +1,113 @@ +"""Tools for interacting with the persistent memory service.""" + +import logging +from typing import Any, Dict, List + +from google.adk.tools import FunctionTool +from google.adk.tools.tool_context import ToolContext + +logger = logging.getLogger(__name__) + + +def add_memory_fact_tool(tool_context: ToolContext, entity_name: str, fact_content: str) -> str: + """Adds a discrete fact to the memory service for the current session.""" + # Access services via _invocation_context + invocation_context = getattr(tool_context, "_invocation_context", None) + if not invocation_context: + logger.error("Invocation context not found in tool context for add_memory_fact.") + return "Error: Invocation context unavailable." + + memory_service = getattr(invocation_context, "memory_service", None) + # Session ID might be available directly on session object within invocation_context? + # Let's check for session first, then session_id directly + session = getattr(invocation_context, "session", None) + session_id = getattr(session, "id", None) if session else None + if not session_id: + # Fallback: check if session_id is directly on invocation_context (less likely) + session_id = getattr(invocation_context, "session_id", None) + + # --- Remove Debug --- # + # logger.warning(f"Inspecting tool_context in add_memory_fact_tool:") + # try: + # context_vars = vars(tool_context) + # logger.warning(f"vars(tool_context): {context_vars}") + # except TypeError: + # logger.warning("vars() failed, likely no __dict__. Trying dir():") + # context_dir = dir(tool_context) + # logger.warning(f"dir(tool_context): {context_dir}") + # --- End Debug --- + + # Old access method: + # memory_service = getattr(tool_context, "memory_service", None) + # session_id = getattr(tool_context, "session_id", None) + + if not memory_service: + logger.error("Memory service not available in invocation context for add_memory_fact.") + return "Error: Memory service is not available." + if not session_id: + logger.error("Session ID not available in invocation context for add_memory_fact.") + return "Error: Session ID is not available." + + # Check if add_observations method exists (duck typing) + if not hasattr(memory_service, "add_observations") or not callable(memory_service.add_observations): + logger.error("Memory service does not support 'add_observations'.") + return "Error: Memory service does not support adding observations." + + try: + # Structure the observation as a dictionary + observation = {"entity": entity_name, "content": fact_content} + logger.info(f"Adding memory fact for session {session_id}: {observation}") + # Call the underlying service method + memory_service.add_observations(session_id=session_id, observations=[observation]) + return f"Okay, I have remembered that {entity_name} is {fact_content}." + + except Exception as e: + logger.exception(f"Error calling memory_service.add_observations: {e}") + return f"Error: Failed to add fact to memory - {e}" + + +def search_memory_facts_tool(tool_context: ToolContext, query: str) -> List[Dict[str, Any]]: + """Searches discrete facts in the memory service for the current session.""" + # Access services via _invocation_context + invocation_context = getattr(tool_context, "_invocation_context", None) + if not invocation_context: + logger.error("Invocation context not found in tool context for search_memory_facts.") + return [{"error": "Invocation context unavailable."}] + + memory_service = getattr(invocation_context, "memory_service", None) + session = getattr(invocation_context, "session", None) + session_id = getattr(session, "id", None) if session else None + if not session_id: + session_id = getattr(invocation_context, "session_id", None) + + # Old access method: + # memory_service = getattr(tool_context, "memory_service", None) + # session_id = getattr(tool_context, "session_id", None) + + if not memory_service: + logger.error("Memory service not available in invocation context for search_memory_facts.") + return [{"error": "Memory service is not available."}] + if not session_id: + logger.error("Session ID not available in invocation context for search_memory_facts.") + return [{"error": "Session ID is not available."}] + + # Check if search_nodes method exists (duck typing) + if not hasattr(memory_service, "search_nodes") or not callable(memory_service.search_nodes): + logger.error("Memory service does not support 'search_nodes'.") + return [{"error": "Memory service does not support searching facts (nodes)."}] + + try: + logger.info(f"Searching memory facts for session {session_id} with query: '{query}'") + results = memory_service.search_nodes(session_id=session_id, query=query) + # Return the list of dictionaries directly + # The agent's prompt will need to guide it on interpreting this list + return results + + except Exception as e: + logger.exception(f"Error calling memory_service.search_nodes: {e}") + return [{"error": f"Failed to search facts in memory - {e}"}] + + +# Wrap functions with FunctionTool +add_memory_fact = FunctionTool(add_memory_fact_tool) +search_memory_facts = FunctionTool(search_memory_facts_tool) diff --git a/contributing/samples/software_engineer_gemini_mcp/tools/persistent_memory_tool.py b/contributing/samples/software_engineer_gemini_mcp/tools/persistent_memory_tool.py new file mode 100644 index 0000000000..5a8d0490c3 --- /dev/null +++ b/contributing/samples/software_engineer_gemini_mcp/tools/persistent_memory_tool.py @@ -0,0 +1,155 @@ +"""Placeholder tools for manually saving/loading session memory to a file.""" + +import logging +from typing import Any, Dict + +from google.adk.tools import FunctionTool, ToolContext + +logger = logging.getLogger(__name__) + +# Default path for the memory file, could be configurable +DEFAULT_MEMORY_FILE = "./.manual_agent_memory.json" + +# === Tool Implementation Functions (Commented Out) === + + +def _save_current_session_to_file_impl(tool_context: ToolContext, filepath: str = DEFAULT_MEMORY_FILE) -> Dict[str, str]: + """ + (Placeholder) Saves the *current* session's state to a specified JSON file. + NOTE: This is a placeholder and not fully implemented. + + Args: + tool_context: The ADK tool context containing session information. + filepath: The path to the JSON file where the session should be saved. + + Returns: + A dictionary indicating the status of the operation. + """ + # TODO: Implement this tool if manual file-based persistence is needed + # for the standard 'adk run' environment. + # Implications: + # - Requires agent to be explicitly prompted to call this tool. + # - Overwrites the file with only the *current* session, or needs logic + # to merge with existing sessions in the file. + # - Doesn't integrate with the ADK's built-in MemoryService. + # - Needs robust error handling (file I/O, JSON serialization). + + logger.warning("Tool 'save_current_session_to_file' is a placeholder and not implemented.") + # --- Begin Commented Implementation Example --- + # if not hasattr(tool_context, 'session') or not tool_context.session: + # msg = "No active session found in tool_context." + # logger.error(msg) + # return {"status": "error", "message": msg} + # + # session: Session = tool_context.session + # logger.info(f"Attempting to save session {session.session_id} to {filepath}...") + # + # # Logic to load existing data, add/update the current session, and save back + # existing_data = {} + # if os.path.exists(filepath): + # try: + # with open(filepath, 'r', encoding='utf-8') as f: + # existing_data = json.load(f) + # logger.debug(f"Loaded {len(existing_data)} sessions from {filepath}") + # except (IOError, json.JSONDecodeError) as e: + # logger.error(f"Error reading existing memory file {filepath}: {e}. Overwriting may occur.") + # + # session_key = f"{session.app_name}_{session.user_id}_{session.session_id}" + # existing_data[session_key] = session.model_dump(mode='json') + # + # try: + # os.makedirs(os.path.dirname(filepath), exist_ok=True) + # with open(filepath, 'w', encoding='utf-8') as f: + # json.dump(existing_data, f, indent=4) + # logger.info(f"Successfully saved session {session.session_id} to {filepath}.") + # return {"status": "success", "message": f"Session saved to {filepath}"} + # except (IOError, TypeError) as e: + # msg = f"Error writing memory file {filepath}: {e}" + # logger.error(msg) + # return {"status": "error", "message": msg} + # --- End Commented Implementation Example --- + return {"status": "skipped", "message": "Tool is not implemented."} + + +def _load_memory_from_file_impl(query: str, filepath: str = DEFAULT_MEMORY_FILE) -> Dict[str, Any]: + """ + (Placeholder) Loads memory from a JSON file and performs a simple query. + NOTE: This is a placeholder and not fully implemented. + + Args: + query: The natural language query to search for in stored messages. + filepath: The path to the JSON file containing stored sessions. + + Returns: + A dictionary containing the search results or an error message. + """ + # TODO: Implement this tool if manual file-based persistence is needed + # for the standard 'adk run' environment. + # Implications: + # - Requires agent to be explicitly prompted to call this tool instead of load_memory. + # - Requires careful design of the query mechanism (e.g., simple substring search). + # - Doesn't integrate with the ADK's built-in MemoryService. + # - Needs robust error handling (file I/O, JSON deserialization, search logic). + + logger.warning("Tool 'load_memory_from_file' is a placeholder and not implemented.") + # --- Begin Commented Implementation Example --- + # if not os.path.exists(filepath): + # msg = f"Memory file not found: {filepath}" + # logger.error(msg) + # return {"status": "error", "message": msg, "results": []} + # + # try: + # with open(filepath, 'r', encoding='utf-8') as f: + # stored_sessions_data: Dict[str, Dict[str, Any]] = json.load(f) + # logger.info(f"Loaded {len(stored_sessions_data)} sessions from {filepath} for query: '{query}'") + # except (IOError, json.JSONDecodeError) as e: + # msg = f"Error reading memory file {filepath}: {e}" + # logger.error(msg) + # return {"status": "error", "message": msg, "results": []} + # + # results: List[Dict[str, Any]] = [] + # query_lower = query.lower() + # + # for session_key, session_data in stored_sessions_data.items(): + # try: + # # Minimal validation - check for history + # history = session_data.get('history', []) + # if not history: + # continue + # + # session_matched = False + # for message in history: + # if isinstance(message, dict) and 'parts' in message: + # message_text = "".join( + # [part.get('text', '') for part in message['parts'] if isinstance(part, dict)] + # ).lower() + # if query_lower in message_text: + # session_matched = True + # break + # + # if session_matched: + # logger.debug(f"Found match in session {session_key}") + # results.append({"session_key": session_key, "session_data": session_data}) + # # Limit results? Maybe return only the first few matches or most relevant? + # + # except Exception as e: + # logger.warning(f"Error processing session {session_key}: {e}. Skipping.") + # + # logger.info(f"Found {len(results)} relevant session(s) for query: '{query}'") + # return {"status": "success", "results": results} + # --- End Commented Implementation Example --- + return {"status": "skipped", "message": "Tool is not implemented.", "results": []} + + +# === Tool Definitions === + +# Wrap the placeholder functions with FunctionTool +save_current_session_to_file_tool = FunctionTool( + func=_save_current_session_to_file_impl, + # Name and description are inferred from the function docstring +) + +load_memory_from_file_tool = FunctionTool( + func=_load_memory_from_file_impl, + # Name and description are inferred from the function docstring +) diff --git a/contributing/samples/software_engineer_gemini_mcp/tools/project_context.py b/contributing/samples/software_engineer_gemini_mcp/tools/project_context.py new file mode 100644 index 0000000000..2d41fbf8fd --- /dev/null +++ b/contributing/samples/software_engineer_gemini_mcp/tools/project_context.py @@ -0,0 +1,54 @@ +"""Project context tool for the software engineer agent.""" + +import json +import os + +from google.adk.agents.callback_context import CallbackContext + +# Define constants +PROJECT_CONTEXT_KEY = "project_context" +USER_PROFILE_KEY = "user_profile" +DEFAULT_CONTEXT_PATH = os.getenv("SOFTWARE_ENGINEER_CONTEXT", "eval/project_context_empty.json") + + +def load_project_context(callback_context: CallbackContext): + """ + Load the project context and user profile from a JSON file. + + Args: + callback_context: The callback context from ADK. + """ + # Initialize empty context + project_context = {} + user_profile = {} + + try: + if os.path.exists(DEFAULT_CONTEXT_PATH): + with open(DEFAULT_CONTEXT_PATH, "r") as file: + data = json.load(file) + project_context = data.get("project_context", {}) + user_profile = data.get("user_profile", {}) + print(f"\nLoaded project context: {project_context}\n") + print(f"\nLoaded user profile: {user_profile}\n") + except Exception as e: + print(f"Error loading project context: {e}") + + # Set the context in the state + callback_context.state[PROJECT_CONTEXT_KEY] = json.dumps(project_context, indent=2) + callback_context.state[USER_PROFILE_KEY] = json.dumps(user_profile, indent=2) + + +def memorize_context(key: str, value: str, context: dict): + """ + Store information in the project context. + + Args: + key: The key to store the value under. + value: The value to store. + context: The context dictionary to update. + + Returns: + A status message. + """ + context[key] = value + return {"status": f'Stored "{key}": "{value}" in project context'} diff --git a/contributing/samples/software_engineer_gemini_mcp/tools/search.py b/contributing/samples/software_engineer_gemini_mcp/tools/search.py new file mode 100644 index 0000000000..6b3141d149 --- /dev/null +++ b/contributing/samples/software_engineer_gemini_mcp/tools/search.py @@ -0,0 +1,34 @@ +# ruff: noqa +# 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. + +"""Wrapper to Google Search Grounding with custom prompt.""" + +from google.adk.agents import Agent +from google.adk.tools.agent_tool import AgentTool +from google.adk.tools.google_search_tool import google_search + +_search_agent = Agent( + model="gemini-2.0-flash", + name="google_search_grounding", + description="An agent providing Google-search grounding capability", + instruction=""" + Answer the user's question directly using google_search grounding tool; Provide a brief but concise response. + Rather than a detail response, provide the immediate actionable item for a software engineer, in a single sentence. + Do not ask the user to check or look up information for themselves, that's your role; do your best to be informative. + """, + tools=[google_search], +) + +google_search_grounding = AgentTool(agent=_search_agent) diff --git a/contributing/samples/software_engineer_gemini_mcp/tools/shell_command.py b/contributing/samples/software_engineer_gemini_mcp/tools/shell_command.py new file mode 100644 index 0000000000..534cb52f99 --- /dev/null +++ b/contributing/samples/software_engineer_gemini_mcp/tools/shell_command.py @@ -0,0 +1,405 @@ +import logging +import shlex +import shutil # <-- Added import +import subprocess +from typing import Literal, Optional + +# Import ToolContext for state management +from google.adk.tools import ( + FunctionTool, # Ensure FunctionTool is imported if not already + ToolContext, +) +from pydantic import BaseModel, Field + +logger = logging.getLogger(__name__) + +# --- Configuration Tool --- # + + +class ConfigureShellApprovalInput(BaseModel): + """Input model for configuring shell command approval.""" + + require_approval: bool = Field(..., description="Set to true to require approval, false to disable.") + + +class ConfigureShellApprovalOutput(BaseModel): + """Output model for configuring shell command approval.""" + + status: str + + +def configure_shell_approval(args: dict, tool_context: ToolContext) -> ConfigureShellApprovalOutput: + """Configures whether running shell commands requires user approval for the current session. + + Args: + args (dict): A dictionary containing: + require_approval (bool): Set to true to require approval, false to disable. + tool_context (ToolContext): The context for accessing session state. + """ + require_approval = args.get("require_approval") + + # Add validation for the boolean argument + if require_approval is None or not isinstance(require_approval, bool): + message = "Error: 'require_approval' argument is missing or not a boolean (true/false)." + logger.error(message) + return ConfigureShellApprovalOutput(status=message) + + tool_context.state["require_shell_approval"] = require_approval + status = "enabled" if require_approval else "disabled" + logger.info(f"Shell command approval requirement set to: {status}") + return ConfigureShellApprovalOutput(status=f"Shell command approval requirement is now {status}.") + + +# --- Whitelist Configuration Tool --- # + + +class ConfigureShellWhitelistInput(BaseModel): + """Input model for configuring the shell command whitelist.""" + + action: Literal["add", "remove", "list", "clear"] = Field(..., description="Action to perform: add, remove, list, or clear.") + command: Optional[str] = Field(None, description="The command to add or remove (required for 'add' and 'remove' actions).") + + +class ConfigureShellWhitelistOutput(BaseModel): + """Output model for configuring the shell command whitelist.""" + + status: str + whitelist: Optional[list[str]] = Field(None, description="The current whitelist (only for 'list' action).") + + +def configure_shell_whitelist(args: dict, tool_context: ToolContext) -> ConfigureShellWhitelistOutput: + """Manages the whitelist of shell commands that bypass approval. + + Args: + args (dict): A dictionary containing: + action (Literal["add", "remove", "list", "clear"]): The action. + command (Optional[str]): The command for add/remove. + tool_context (ToolContext): The context for accessing session state. + """ + action = args.get("action") + command = args.get("command") + + # Default safe commands (adjust as needed) + DEFAULT_SAFE_COMMANDS = [ + "ls", + "grep", + "find", + "cat", + "pwd", + "echo", + "git status", + "head", + "tail", + "wc", + "git diff", + "git log", + "which", + "ping", + "host", + "dig", + "nslookup", + "ss", + "uname", + "uptime", + "date", + "df", + "du", + "free", + "stat", + "ps", + "pgrep", + "ip addr", + "ip route", + "traceroute", + "git grep", + "git branch", + "git branch --show-current", # Specific safe variant + "git tag", + "git remote -v", + "git config --list", + "docker ps", + "docker images", + "kubectl get", + "kubectl describe", + "kubectl logs", + "kubectl cluster-info", + "kubectl config view", + "kubectl version", + "kubectl api-resources", + "kubectl api-versions", + "kubectl top", + ] + + # Initialize whitelist in state if it doesn't exist + if "shell_command_whitelist" not in tool_context.state: + # Initialize with default safe commands + tool_context.state["shell_command_whitelist"] = DEFAULT_SAFE_COMMANDS[:] + logger.info(f"Initialized shell command whitelist with defaults: {DEFAULT_SAFE_COMMANDS}") + + whitelist: list[str] = tool_context.state["shell_command_whitelist"] + + if action == "add": + if not command: + return ConfigureShellWhitelistOutput(status="Error: 'command' is required for 'add' action.") + if command not in whitelist: + whitelist.append(command) + tool_context.state["shell_command_whitelist"] = whitelist # Update state + logger.info(f"Added command '{command}' to shell whitelist.") + return ConfigureShellWhitelistOutput(status=f"Command '{command}' added to whitelist.") + else: + return ConfigureShellWhitelistOutput(status=f"Command '{command}' is already in the whitelist.") + elif action == "remove": + if not command: + return ConfigureShellWhitelistOutput(status="Error: 'command' is required for 'remove' action.") + if command in whitelist: + whitelist.remove(command) + tool_context.state["shell_command_whitelist"] = whitelist # Update state + logger.info(f"Removed command '{command}' from shell whitelist.") + return ConfigureShellWhitelistOutput(status=f"Command '{command}' removed from whitelist.") + else: + return ConfigureShellWhitelistOutput(status=f"Command '{command}' not found in whitelist.") + elif action == "list": + return ConfigureShellWhitelistOutput(status="Current whitelist retrieved.", whitelist=list(whitelist)) # Return a copy + elif action == "clear": + tool_context.state["shell_command_whitelist"] = [] + logger.info("Cleared shell command whitelist.") + return ConfigureShellWhitelistOutput(status="Shell command whitelist cleared.") + else: + return ConfigureShellWhitelistOutput(status=f"Error: Invalid action '{action}'. Valid actions are: add, remove, list, clear.") + + +# --- Check Command Existence Tool --- # <--- Added section start + + +class CheckCommandExistsInput(BaseModel): + """Input model for checking command existence.""" + + command: str = Field(..., description="The command name (e.g., 'git', 'ls') to check for existence.") + + +class CheckCommandExistsOutput(BaseModel): + """Output model for checking command existence.""" + + exists: bool + command_checked: str + message: str + + +def check_command_exists(args: dict, tool_context: ToolContext) -> CheckCommandExistsOutput: + """Checks if a command exists in the system's PATH. Extracts the base command.""" + command_name = args.get("command") + base_command = None + message = "" + + if not command_name: + message = "Error: 'command' argument is missing." + logger.error(message) + return CheckCommandExistsOutput(exists=False, command_checked=command_name or "", message=message) + + try: + # Extract base command if it includes arguments (shutil.which needs the command name only) + parts = shlex.split(command_name) + if parts: + base_command = parts[0] + else: + message = f"Could not parse base command from input: '{command_name}'" + logger.warning(message) + return CheckCommandExistsOutput(exists=False, command_checked=command_name, message=message) + + except ValueError as e: + message = f"Error parsing command '{command_name}': {e}" + logger.error(message) + return CheckCommandExistsOutput(exists=False, command_checked=command_name, message=message) + + if not base_command: # Should not happen if parsing worked, but check anyway + message = "Error: Could not determine base command." + logger.error(message) + return CheckCommandExistsOutput(exists=False, command_checked=command_name, message=message) + + exists = shutil.which(base_command) is not None + status_msg = "exists" if exists else "does not exist" + message = f"Command '{base_command}' {status_msg} in system PATH." + logger.info(f"Checked existence for command '{base_command}': {exists}") + return CheckCommandExistsOutput(exists=exists, command_checked=base_command, message=message) + + +# <--- Added section end + + +# --- Shell Command Safety Check Tool --- # + + +class CheckShellCommandSafetyInput(BaseModel): + """Input model for checking shell command safety.""" + + command: str = Field(..., description="The shell command to check.") + + +class CheckShellCommandSafetyOutput(BaseModel): + """Output model for checking shell command safety.""" + + status: Literal["whitelisted", "approval_disabled", "approval_required"] = Field(..., description="The safety status of the command.") + command: str = Field(..., description="The command that was checked.") + message: str = Field(..., description="Explanation of the status.") + + +def check_shell_command_safety(args: dict, tool_context: ToolContext) -> CheckShellCommandSafetyOutput: + """Checks if a shell command is safe to run without explicit user approval. + + Checks against the configured whitelist and the session's approval requirement. + Does NOT execute the command. + + Args: + args (dict): A dictionary containing: + command (str): The shell command to check. + tool_context (ToolContext): The context for accessing session state. + + Returns: + CheckShellCommandSafetyOutput: An object indicating the safety status. + """ + command = args.get("command") + if not command: + # Technically this shouldn't happen with Pydantic validation, but belt-and-suspenders + return CheckShellCommandSafetyOutput( + status="approval_required", # Default to safest option on error + command=command or "", + message="Error: Command argument missing in input.", + ) + + require_approval = tool_context.state.get("require_shell_approval", True) + # Ensure whitelist is initialized if needed (accessing it via configure_shell_whitelist initializes) + if "shell_command_whitelist" not in tool_context.state: + # Temporarily call configure_shell_whitelist with 'list' action to initialize state + # This is a slight workaround to ensure initialization happens if only check/execute are called. + # A cleaner approach might involve a dedicated initialization step or context manager. + _ = configure_shell_whitelist({"action": "list"}, tool_context) + + shell_whitelist = tool_context.state.get("shell_command_whitelist", []) + is_whitelisted = command in shell_whitelist + + if is_whitelisted: + logger.info(f"Command '{command}' is whitelisted.") + return CheckShellCommandSafetyOutput(status="whitelisted", command=command, message="Command is in the configured whitelist and can be run directly.") + elif not require_approval: + logger.info(f"Command '{command}' is not whitelisted, but shell approval is disabled.") + return CheckShellCommandSafetyOutput( + status="approval_disabled", command=command, message="Command is not whitelisted, but approval is disabled for this session." + ) + else: + logger.warning(f"Command '{command}' requires approval (not whitelisted and approval enabled).") + return CheckShellCommandSafetyOutput( + status="approval_required", command=command, message="Command requires user approval as it is not whitelisted and approval is enabled." + ) + + +# --- Vetted Shell Command Execution Tool --- # + + +class ExecuteVettedShellCommandInput(BaseModel): + """Input model for the execute_vetted_shell_command tool.""" + + command: str = Field(..., description="The shell command to execute. Should have been vetted first.") + working_directory: Optional[str] = Field(None, description="Optional working directory to run the command in.") + timeout: int = Field(60, description="Timeout in seconds for the command execution.") + + +class ExecuteVettedShellCommandOutput(BaseModel): + """Output model for the execute_vetted_shell_command tool.""" + + stdout: str | None = Field(None, description="The standard output of the command.") + stderr: str | None = Field(None, description="The standard error of the command.") + return_code: int | None = Field(None, description="The return code of the command.") + command_executed: str | None = Field(None, description="The command that was executed.") + status: str = Field(description="Status: 'executed' or 'error'.") + message: str = Field(description="Additional information about the status.") + + +def execute_vetted_shell_command(args: dict, tool_context: ToolContext) -> ExecuteVettedShellCommandOutput: + """Executes a shell command that has ALREADY BEEN VETTED or explicitly approved. + + ***WARNING:*** DO NOT CALL THIS TOOL directly unless you have either: + 1. Called `check_shell_command_safety` and received a status of 'whitelisted' or 'approval_disabled'. + 2. Received explicit user confirmation to run this specific command. + + This tool performs NO safety checks itself. + + Args: + args (dict): A dictionary containing: + command (str): The shell command to execute. + working_directory (Optional[str]): Optional working directory. + timeout (Optional[int]): Optional timeout in seconds (default: 60). + tool_context (ToolContext): The context (unused here, but required by ADK). + + Returns: + ExecuteVettedShellCommandOutput: The result of the command execution. + """ + command = args.get("command") + working_directory = args.get("working_directory") + timeout = args.get("timeout", 60) + + if not command: + return ExecuteVettedShellCommandOutput(status="error", command_executed=command, message="Error: 'command' argument is missing.") + + try: + timeout_sec = int(timeout) + except (ValueError, TypeError): + return ExecuteVettedShellCommandOutput( + status="error", command_executed=command, message=f"Error: Invalid timeout value '{timeout}'. Must be an integer." + ) + + command_parts = shlex.split(command) + logger.info(f"Executing vetted shell command: '{command}' in directory '{working_directory or '.'}'") + + try: + process = subprocess.run( + command_parts, + capture_output=True, + text=True, + cwd=working_directory, + timeout=timeout_sec, + check=False, # Don't raise exception on non-zero exit + ) + logger.info(f"Vetted command '{command}' finished with return code {process.returncode}") + return ExecuteVettedShellCommandOutput( + stdout=process.stdout.strip(), + stderr=process.stderr.strip(), + return_code=process.returncode, + command_executed=command, + status="executed", + message="Command executed successfully." if process.returncode == 0 else "Command executed with non-zero exit code.", + ) + except FileNotFoundError: + logger.error(f"Command not found during execution: {command_parts[0]}") + return ExecuteVettedShellCommandOutput( + stderr=f"Error: Command not found: {command_parts[0]}", + return_code=-1, # Using distinct negative codes for different errors + command_executed=command, + status="error", + message=f"Command not found: {command_parts[0]}", + ) + except subprocess.TimeoutExpired: + logger.error(f"Vetted command '{command}' timed out after {timeout_sec} seconds.") + return ExecuteVettedShellCommandOutput( + stderr=f"Error: Command timed out after {timeout_sec} seconds.", + return_code=-2, + command_executed=command, + status="error", + message=f"Command timed out after {timeout_sec} seconds.", + ) + except Exception as e: + logger.exception(f"An unexpected error occurred while running vetted command '{command}': {e}") + return ExecuteVettedShellCommandOutput( + stderr=f"An unexpected error occurred: {e}", return_code=-3, command_executed=command, status="error", message=f"An unexpected error occurred: {e}" + ) + + +# --- Tool Registrations --- # <-- Added section (optional but good practice) + +# Wrap functions with FunctionTool +# Note: This assumes FunctionTool is imported or available in the scope + +configure_shell_approval_tool = FunctionTool(configure_shell_approval) +configure_shell_whitelist_tool = FunctionTool(configure_shell_whitelist) +check_command_exists_tool = FunctionTool(check_command_exists) # <-- Added tool +check_shell_command_safety_tool = FunctionTool(check_shell_command_safety) +execute_vetted_shell_command_tool = FunctionTool(execute_vetted_shell_command) diff --git a/contributing/samples/software_engineer_gemini_mcp/tools/system_info.py b/contributing/samples/software_engineer_gemini_mcp/tools/system_info.py new file mode 100644 index 0000000000..7e236e4f4e --- /dev/null +++ b/contributing/samples/software_engineer_gemini_mcp/tools/system_info.py @@ -0,0 +1,31 @@ +import logging +import platform + +from google.adk.tools import FunctionTool +from pydantic import BaseModel, Field + +logger = logging.getLogger(__name__) + + +class OSInfoOutput(BaseModel): + """Output model for the get_os_info tool.""" + + system: str = Field(description="Operating system name (e.g., 'Linux', 'Darwin', 'Windows').") + release: str = Field(description="Operating system release (e.g., '5.15.0-78-generic').") + version: str = Field(description="Operating system version.") + machine: str = Field(description="Machine hardware name (e.g., 'x86_64').") + + +def get_os_info() -> OSInfoOutput: + """Gets basic operating system information.""" + logger.info("Getting operating system information.") + try: + return OSInfoOutput(system=platform.system(), release=platform.release(), version=platform.version(), machine=platform.machine()) + except Exception as e: + logger.exception(f"Failed to get OS info: {e}") + # Return placeholder values on error + return OSInfoOutput(system="Unknown", release="Unknown", version="Unknown", machine="Unknown") + + +# Wrap get_os_info with FunctionTool +get_os_info_tool = FunctionTool(get_os_info) diff --git a/contributing/samples/software_engineer_litellm_llama/README.md b/contributing/samples/software_engineer_litellm_llama/README.md new file mode 100644 index 0000000000..d9c521fb98 --- /dev/null +++ b/contributing/samples/software_engineer_litellm_llama/README.md @@ -0,0 +1,86 @@ +# Software Engineer Agent (LiteLLM - Multi-Model Compatible) + +This agent uses LiteLLM to provide software engineering assistance with multi-model compatibility, supporting both Gemini and LLaMA models. + +## Model Compatibility + +This agent is designed to work with different LLM backends with specific adaptations for cross-model compatibility: + +### Key Compatibility Features + +1. **Flexible Tool Arguments** + - Tool functions handle both dictionary and string arguments + - Includes argument parsing utilities to handle model-specific formats + - String-to-boolean conversion for handling "true"/"false" string literals + +2. **Tool Discovery Mechanism** + - LLaMA models often try to call discovery functions like `list_available_tools` before using tools + - Gemini models typically work with directly registered tools + - Solution: Implemented a flexible tool discovery system with multiple aliases: + - `list_available_tools_tool` (primary name) + - `list_tools_tool` (shorter alias) + - `available_tools_tool` (another common variation) + +3. **Clear Tool Documentation** + - Explicit tool listing in the prompt instructions + - Comprehensive tool descriptions + - Instructions to avoid discovery calls when possible + +### Implementation Details + +#### Tool Discovery Function + +The tool discovery function returns a structured listing of all available tools: + +```python +def list_available_tools(args: dict, tool_context: ToolContext) -> Dict[str, Any]: + """Lists all available tools in the agent.""" + tools = { + "file_system_tools": { ... }, + "shell_command_tools": { ... }, + "search_tools": { ... }, + "system_info_tools": { ... } + } + + return { + "tools": tools, + "message": "Use these tools directly by calling them with appropriate arguments." + } +``` + +#### Registered with Multiple Aliases + +```python +# Primary tool +list_available_tools_tool = FunctionTool(list_available_tools) + +# Aliases for different naming conventions +list_tools_tool = list_available_tools_tool +available_tools_tool = list_available_tools_tool +``` + +## Tool Categories + +The agent provides tools in several categories: + +1. **File System Tools**: Read, list, edit files and configure approval requirements +2. **Shell Command Tools**: Execute shell commands with safety checks and approval settings +3. **Search Tools**: Search the web and codebase +4. **System Info Tools**: Get OS information and list available tools + +## Usage + +To run this agent with a specific model: + +```bash +uv run code-agent run "-" custom_agents/software_engineer_litellm_llama/software_engineer +``` + +The agent is configured to use LiteLLM which provides a unified interface to many LLM backends. + +### Key Benefits + +- Works with both Gemini and LLaMA models without modifications +- Handles different argument formats transparently +- Provides tool discovery for models that require it +- Maintains backward compatibility with existing code \ No newline at end of file diff --git a/contributing/samples/software_engineer_litellm_llama/__init__.py b/contributing/samples/software_engineer_litellm_llama/__init__.py new file mode 100644 index 0000000000..d9ec974bc2 --- /dev/null +++ b/contributing/samples/software_engineer_litellm_llama/__init__.py @@ -0,0 +1,10 @@ +# ruff: noqa +""" +Software Engineer Agent. + +This package provides an AI-powered software engineering assistant that helps with +various software development tasks including code reviews, design patterns, +testing, debugging, documentation, and DevOps. +""" + +from . import agent diff --git a/contributing/samples/software_engineer_litellm_llama/agent.py b/contributing/samples/software_engineer_litellm_llama/agent.py new file mode 100644 index 0000000000..bbfa21380a --- /dev/null +++ b/contributing/samples/software_engineer_litellm_llama/agent.py @@ -0,0 +1,109 @@ +"""Implementation of the Software Engineer Agent with knowledge and experience of sub-agents.""" + +import logging + +import os + +from google.adk.agents.llm_agent import LlmAgent +from google.adk.models.lite_llm import LiteLlm +from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset +from google.adk.tools.mcp_tool.mcp_toolset import StdioServerParameters + +# from . import prompt + +# Use relative imports from the 'software_engineer' sibling directory +# from .sub_agents.code_quality.agent import code_quality_agent +# from .sub_agents.code_review.agent import code_review_agent +# from .sub_agents.debugging.agent import debugging_agent +# from .sub_agents.design_pattern.agent import design_pattern_agent +# from .sub_agents.devops.agent import devops_agent +# from .sub_agents.documentation.agent import documentation_agent +# from .sub_agents.testing.agent import testing_agent +# from .tools import ( +# available_tools_tool, +# check_command_exists_tool, +# check_shell_command_safety_tool, +# codebase_search_tool, +# configure_shell_approval_tool, +# configure_shell_whitelist_tool, +# edit_file_tool, +# execute_vetted_shell_command_tool, +# get_os_info_tool, +# google_search_grounding, +# list_available_tools_tool, +# list_dir_tool, +# list_tools_tool, +# read_file_tool, +# ) + +# Import tools via the tools package __init__ +# from .tools import ( +# configure_approval_tool as configure_edit_approval_tool, # Keep alias for now +# ) + +# from .tools.project_context import load_project_context + +# logger = logging.getLogger(__name__) + + +# --- Agent Definition --- + +# Model name as recognized by *your* vLLM endpoint configuration +# model_name_at_endpoint = "ollama_chat/llama3.2" # Actually does not work as documented on ADK. +model_name_at_endpoint = "hosted_vllm/llama3.2" # Example from vllm_test.py + +# REF: https://google.github.io/adk-docs/agents/models/#using-open-local-models-via-litellm +root_agent = LlmAgent( + model=LiteLlm(model=model_name_at_endpoint), + name="coordinator_agent", + description="An AI software engineer assistant that helps with various software development tasks", + instruction=""" + - You are a software engineer assistant + - You help and lead developers with various software development tasks including code reviews, design patterns, testing, debugging, documentation, and DevOps + - Format your responses back to users with markdown. Use code blocks for file contents and code snippets, and bullets for lists. + - After every tool call, summarize the result and keep your response concise + """, + # instruction=prompt.ROOT_AGENT_INSTR, + # sub_agents=[ + # design_pattern_agent, + # documentation_agent, + # code_review_agent, + # code_quality_agent, + # testing_agent, + # debugging_agent, + # devops_agent, # TODO: Move command tools to devops_agent with more guardrails + # ], + tools=[ + # read_file_tool, + # list_dir_tool, + # edit_file_tool, + # configure_edit_approval_tool, + # check_command_exists_tool, + # check_shell_command_safety_tool, + # configure_shell_approval_tool, + # configure_shell_whitelist_tool, + # execute_vetted_shell_command_tool, + # google_search_grounding, + # codebase_search_tool, + # get_os_info_tool, + # list_available_tools_tool, # NOTE: This is needed for LiteLLM models in order to use the FunctionTool. + # list_tools_tool, + # available_tools_tool, + MCPToolset( + connection_params=StdioServerParameters( + command='npx', + args=[ + '-y', # Arguments for the command + '@modelcontextprotocol/server-filesystem', + os.path.dirname(os.path.abspath(__file__)), + ], + ), + # don't want agent to do write operation + # tool_predicate=lambda tool, ctx=None: tool.name + # not in ('write_file', 'edit_file', 'create_directory', 'move_file'), + ), + ], + # Pass the function directly, not as a list + # before_agent_callback=load_project_context, + # output_key="software_engineer", +) diff --git a/contributing/samples/software_engineer_litellm_llama/prompt.py b/contributing/samples/software_engineer_litellm_llama/prompt.py new file mode 100644 index 0000000000..569af61b8a --- /dev/null +++ b/contributing/samples/software_engineer_litellm_llama/prompt.py @@ -0,0 +1,88 @@ +# ruff: noqa +"""Defines the prompts for the software engineer agent.""" + +ROOT_AGENT_INSTR = """ +- You are a software engineer assistant +- You help and lead developers with various software development tasks including code reviews, design patterns, testing, debugging, documentation, and DevOps +- You delegate tasks to the appropriate sub-agents based on the user's request +- Format your responses back to users with markdown. Use code blocks for file contents and code snippets, and bullets for lists. +- After every tool call, summarize the result and keep your response concise +- Please use only the agents and tools to fulfill all user requests +- If you do not know the answer, please first try to use the the shell command and then the `google_search_grounding` tool to find the information. + +## Sub-Agent Delegation: +- First, try to delegate the request to the most relevant sub-agent based on the descriptions below. +- Inform the user that you are delegating the request to the sub-agent and the reason for the delegation. +- If the user asks for code review, transfer to the agent `code_review_agent` +- If the user asks for code quality analysis, static analysis, or quality improvements, transfer to the agent `code_quality_agent` +- If the user asks about design patterns or architecture, transfer to the agent `design_pattern_agent` +- If the user asks about testing, test generation, or test strategies, transfer to the agent `testing_agent` +- If the user asks for help with debugging or fixing errors, transfer to the agent `debugging_agent` +- If the user asks for help with documentation, transfer to the agent `documentation_agent` +- If the user asks about deployment, CI/CD, or DevOps practices, transfer to the agent `devops_agent` +""" + +# ROOT_AGENT_INSTR = """ +# - You are an autonomous principal software engineer assistant +# - You help and lead developers with various software development tasks including code reviews, design patterns, testing, debugging, documentation, and DevOps +# - You delegate tasks to the appropriate sub-agents based on the user's request +# - Format your responses back to users with markdown. Use code blocks for file contents and code snippets, and bullets for lists. +# - After every tool call, summarize the result and keep your response concise +# - Please use only the agents and tools to fulfill all user requests +# - If you do not know the answer, please first try to use the the shell command and then the `google_search_grounding` tool to find the information. + +# ## Available Tools +# You have direct access to the following tools - do not attempt to call any discovery functions: +# - **File System Tools**: `read_file_tool`, `list_dir_tool`, `edit_file_tool`, `configure_edit_approval_tool` +# - **Shell Command Tools**: `check_command_exists_tool`, `check_shell_command_safety_tool`, `configure_shell_approval_tool`, `configure_shell_whitelist_tool`, `execute_vetted_shell_command_tool` +# - **Search Tools**: `google_search_grounding`, `codebase_search_tool` +# - **System Info**: `get_os_info_tool` + +# ## Other Tools: +# - If you cannot delegate the request to a sub-agent, or if the query is about a general topic you don't know, use the `google_search_grounding` tool to find the information. +# - To search through code, use the `codebase_search_tool` tool. +# - To get system information, use the `get_os_info_tool` tool. + +# ## File System Interactions: +# - To list files or directories, use the `list_dir_tool` tool. Provide the path. +# - To read a file, use the `read_file_tool` tool. Provide the path. +# - **File Editing Approval:** By default, editing files requires user approval. You can change this setting for the current session using the `configure_edit_approval_tool` tool. Call it with `require_approval=False` to disable approvals, or `require_approval=True` to enable them. +# - **Editing/Creating Files:** To edit an existing file or create a new one, use the `edit_file_tool` tool. Provide the `filepath` and the full `content`. +# - If approval is required (default or enabled via `configure_edit_approval_tool`), this tool will return a `pending_approval` status. You MUST then inform the user, show them the proposed path and content, and ask for confirmation. +# - If the user approves, call `edit_file_tool` again with the exact same `filepath` and `content`. +# - If approval is *not* required (disabled via `configure_edit_approval_tool`), the tool will write the file directly. + +# ## Shell Command Execution: +# - **Available Tools:** +# - `configure_shell_approval_tool`: Enables or disables the need for user approval for NON-WHITELISTED commands (Default: enabled, `require_approval=True`). +# - `configure_shell_whitelist_tool`: Manages a list of commands that ALWAYS run directly, bypassing the approval check (Actions: `add`, `remove`, `list`, `clear`). A default set of safe commands is included. +# - `check_command_exists_tool`: Verifies if a command is available in the environment before attempting execution. +# - `check_shell_command_safety_tool`: Checks if a specific command can run without explicit user approval based on the whitelist and approval settings. Returns status: `whitelisted`, `approval_disabled`, or `approval_required`. **Use this BEFORE attempting execution.** +# - `execute_vetted_shell_command_tool`: Executes a vetted shell command. This is the **ONLY** way to run shell commands. + +# - **Workflow for Running a Command (``):** +# 1. **Check Existence:** Always run `check_command_exists_tool(command=)` first. If it doesn't exist, inform the user and stop. +# 2. **Check Safety:** Then, run `check_shell_command_safety_tool(command=)`. Review the safety analysis. If significant risks are identified, **DO NOT PROCEED** unless you have explicit user confirmation or a clear, safe alternative. Explain the risks. +# 3. **Execute:** If checks pass, use `execute_vetted_shell_command_tool(command=, rationale=)`. Provide a clear rationale. + +# ## Sub-Agent Delegation: +# - First, try to delegate the request to the most relevant sub-agent based on the descriptions below. +# - Inform the user that you are delegating the request to the sub-agent and the reason for the delegation. +# - If the user asks for code review, transfer to the agent `code_review_agent` +# - If the user asks for code quality analysis, static analysis, or quality improvements, transfer to the agent `code_quality_agent` +# - If the user asks about design patterns or architecture, transfer to the agent `design_pattern_agent` +# - If the user asks about testing, test generation, or test strategies, transfer to the agent `testing_agent` +# - If the user asks for help with debugging or fixing errors, transfer to the agent `debugging_agent` +# - If the user asks for help with documentation, transfer to the agent `documentation_agent` +# - If the user asks about deployment, CI/CD, or DevOps practices, transfer to the agent `devops_agent` + +# Current user: +# +# {user_profile} +# + +# Current project: +# +# {project_context} +# +# """ diff --git a/contributing/samples/software_engineer_litellm_llama/shared_libraries/__init__.py b/contributing/samples/software_engineer_litellm_llama/shared_libraries/__init__.py new file mode 100644 index 0000000000..3d76d32584 --- /dev/null +++ b/contributing/samples/software_engineer_litellm_llama/shared_libraries/__init__.py @@ -0,0 +1 @@ +"""Shared libraries for the software engineer agent.""" diff --git a/contributing/samples/software_engineer_litellm_llama/shared_libraries/types.py b/contributing/samples/software_engineer_litellm_llama/shared_libraries/types.py new file mode 100644 index 0000000000..12c0698b6f --- /dev/null +++ b/contributing/samples/software_engineer_litellm_llama/shared_libraries/types.py @@ -0,0 +1,123 @@ +"""Type definitions for the software engineer agent.""" + +from typing import List, Optional + +from google.genai.types import GenerateContentConfig +from pydantic import BaseModel, Field + +# Configure JSON response format +json_response_config = GenerateContentConfig( + temperature=0.2, + top_p=0.95, + candidate_count=1, +) + + +# https://google.github.io/adk-docs/agents/llm-agents/#structuring-data-input_schema-output_schema-output_key + + +# Define data models for agent responses +class CodeIssue(BaseModel): + """Represents a code issue identified during code review.""" + + issue_type: str = Field(description="Type of issue (bug, security, performance, style)") + severity: str = Field(description="Severity of the issue (critical, high, medium, low)") + location: str = Field(description="File and line number where the issue occurs") + description: str = Field(description="Detailed description of the issue") + recommendation: str = Field(description="Suggested fix or improvement") + + +class CodeReviewResponse(BaseModel): + """Response model for code review analysis.""" + + issues: List[CodeIssue] = Field(description="List of identified code issues") + summary: str = Field(description="Overall summary of the code review") + suggestions: List[str] = Field(description="General suggestions for improvement") + + +class DesignPattern(BaseModel): + """Represents a design pattern recommendation.""" + + pattern_name: str = Field(description="Name of the design pattern") + category: str = Field(description="Category of the pattern (creational, structural, behavioral)") + problem_solved: str = Field(description="What problem this pattern solves") + benefits: List[str] = Field(description="Benefits of using this pattern") + tradeoffs: List[str] = Field(description="Potential drawbacks or tradeoffs") + example_code: str = Field(description="Example implementation code") + + +class DesignPatternResponse(BaseModel): + """Response model for design pattern recommendations.""" + + recommended_patterns: List[DesignPattern] = Field(description="List of recommended design patterns") + explanation: str = Field(description="Explanation of why these patterns are recommended") + + +class TestCase(BaseModel): + """Represents a test case.""" + + name: str = Field(description="Name of the test case") + description: str = Field(description="Description of what the test verifies") + test_type: str = Field(description="Type of test (unit, integration, system)") + prerequisites: List[str] = Field(description="Prerequisites for running the test") + test_code: str = Field(description="The test code implementation") + expected_outcome: str = Field(description="Expected outcome of the test") + + +class TestingResponse(BaseModel): + """Response model for test generation.""" + + test_cases: List[TestCase] = Field(description="List of generated test cases") + testing_strategy: str = Field(description="Overall testing strategy") + test_coverage: Optional[str] = Field(description="Expected test coverage") + + +class DebuggingStep(BaseModel): + """Represents a debugging step.""" + + step_number: int = Field(description="Step number in the debugging process") + description: str = Field(description="Description of the debugging step") + expected_outcome: str = Field(description="What to look for or expect from this step") + code_example: Optional[str] = Field(description="Example code for this debugging step") + + +class DebuggingResponse(BaseModel): + """Response model for debugging assistance.""" + + problem_analysis: str = Field(description="Analysis of the problem") + root_cause: Optional[str] = Field(description="Identified root cause") + debugging_steps: List[DebuggingStep] = Field(description="Steps to debug the issue") + solution: Optional[str] = Field(description="Proposed solution") + + +class DocumentationItem(BaseModel): + """Represents a documentation item.""" + + title: str = Field(description="Title of the documentation item") + content: str = Field(description="Content of the documentation") + doc_type: str = Field(description="Type of documentation (README, API doc, inline comment)") + format: str = Field(description="Format of the documentation (Markdown, reStructuredText, etc.)") + + +class DocumentationResponse(BaseModel): + """Response model for documentation generation.""" + + documentation_items: List[DocumentationItem] = Field(description="List of documentation items") + suggestions: Optional[List[str]] = Field(description="Suggestions for improving documentation") + + +class DevOpsComponent(BaseModel): + """Represents a DevOps component recommendation.""" + + component_name: str = Field(description="Name of the DevOps component") + purpose: str = Field(description="Purpose of this component") + implementation: str = Field(description="Implementation details or configuration") + alternatives: Optional[List[str]] = Field(description="Alternative options") + + +class DevOpsResponse(BaseModel): + """Response model for DevOps recommendations.""" + + components: List[DevOpsComponent] = Field(description="List of DevOps components") + implementation_plan: str = Field(description="Overall implementation plan") + resources: Optional[List[str]] = Field(description="Helpful resources or documentation") diff --git a/contributing/samples/software_engineer_litellm_llama/sub_agents/__init__.py b/contributing/samples/software_engineer_litellm_llama/sub_agents/__init__.py new file mode 100644 index 0000000000..2cd1ec8359 --- /dev/null +++ b/contributing/samples/software_engineer_litellm_llama/sub_agents/__init__.py @@ -0,0 +1 @@ +"""Sub-agents for the software engineer agent.""" diff --git a/contributing/samples/software_engineer_litellm_llama/sub_agents/code_quality/__init__.py b/contributing/samples/software_engineer_litellm_llama/sub_agents/code_quality/__init__.py new file mode 100644 index 0000000000..46a49ae7e0 --- /dev/null +++ b/contributing/samples/software_engineer_litellm_llama/sub_agents/code_quality/__init__.py @@ -0,0 +1 @@ +"""Code quality sub-agent for analyzing code and suggesting improvements.""" diff --git a/contributing/samples/software_engineer_litellm_llama/sub_agents/code_quality/agent.py b/contributing/samples/software_engineer_litellm_llama/sub_agents/code_quality/agent.py new file mode 100644 index 0000000000..ae68e28ee2 --- /dev/null +++ b/contributing/samples/software_engineer_litellm_llama/sub_agents/code_quality/agent.py @@ -0,0 +1,31 @@ +"""Code quality agent implementation.""" + +from google.adk.agents import Agent +from google.genai.types import GenerateContentConfig + +from ...tools.code_analysis import ( + analyze_code_tool, + get_analysis_issues_by_severity_tool, + suggest_code_fixes_tool, +) +from ...tools.filesystem import list_dir_tool, read_file_tool +from . import prompt + +code_quality_agent = Agent( + model="gemini-2.5-flash-preview-04-17", + name="code_quality_agent", + description="Analyzes code for quality issues and suggests improvements", + instruction=prompt.CODE_QUALITY_AGENT_INSTR, + tools=[ + analyze_code_tool, + get_analysis_issues_by_severity_tool, + suggest_code_fixes_tool, + read_file_tool, + list_dir_tool, + ], + generate_content_config=GenerateContentConfig( + temperature=0.1, + top_p=0.95, + max_output_tokens=4096, + ), +) diff --git a/contributing/samples/software_engineer_litellm_llama/sub_agents/code_quality/prompt.py b/contributing/samples/software_engineer_litellm_llama/sub_agents/code_quality/prompt.py new file mode 100644 index 0000000000..6b46127d66 --- /dev/null +++ b/contributing/samples/software_engineer_litellm_llama/sub_agents/code_quality/prompt.py @@ -0,0 +1,44 @@ +# ruff: noqa +"""Prompts for the code quality sub-agent.""" + +CODE_QUALITY_AGENT_INSTR = """ +You are a Code Quality Expert specialized in analyzing code for quality issues, technical debt, and suggesting improvements. + +Your primary responsibilities include: + +1. Analyzing code using static analysis tools to identify issues like bugs, code smells, style violations, + security vulnerabilities, and complexity problems. + +2. Categorizing and prioritizing issues based on severity (critical, error, warning, info). + +3. Explaining detected issues in a way that helps developers understand the problem and how to fix it. + +4. Suggesting specific code improvements and refactorings to address identified issues. + +5. Providing actionable recommendations to improve overall code quality. + +6. Identifying patterns of issues that might indicate deeper architectural or design problems. + +7. Highlighting security vulnerabilities and suggesting secure coding practices. + +8. Analyzing code complexity and suggesting ways to simplify complicated code. + +When a user asks you to analyze code: +1. Use the analyze_code_tool to perform static analysis on the specified file +2. Review the issues and metrics returned by the analysis +3. Use get_analysis_issues_by_severity_tool to retrieve issues filtered by severity if needed +4. Use suggest_code_fixes_tool to generate suggestions for fixing identified issues +5. Provide a concise summary of the code quality assessment +6. Focus on the most critical issues first, then errors, warnings, and finally informational issues +7. Include specific, actionable recommendations for improving the code + +Remember that your goal is to help developers write better, cleaner, more maintainable code. +You should be thorough in your analysis but also practical in your recommendations. + +## Context: + +Current project context: + +{project_context} + +""" diff --git a/contributing/samples/software_engineer_litellm_llama/sub_agents/code_review/__init__.py b/contributing/samples/software_engineer_litellm_llama/sub_agents/code_review/__init__.py new file mode 100644 index 0000000000..0d3416fc05 --- /dev/null +++ b/contributing/samples/software_engineer_litellm_llama/sub_agents/code_review/__init__.py @@ -0,0 +1 @@ +"""Code review agent for analyzing and improving code quality.""" diff --git a/contributing/samples/software_engineer_litellm_llama/sub_agents/code_review/agent.py b/contributing/samples/software_engineer_litellm_llama/sub_agents/code_review/agent.py new file mode 100644 index 0000000000..d85f831156 --- /dev/null +++ b/contributing/samples/software_engineer_litellm_llama/sub_agents/code_review/agent.py @@ -0,0 +1,22 @@ +"""Code review agent implementation.""" + +from google.adk.agents import Agent +from google.genai.types import GenerateContentConfig + +# from software_engineer.sub_agents.code_review.shared_libraries.types import CodeReviewResponse +from ...tools.code_analysis import analyze_code_tool +from ...tools.filesystem import list_dir_tool, read_file_tool +from . import prompt + +code_review_agent = Agent( + model="gemini-2.5-flash-preview-04-17", # "gemini-2.5-pro-exp-03-25", #"gemini-2.0-flash-001", + name="code_review_agent", + description="Analyzes code for issues and suggests improvements", + instruction=prompt.CODE_REVIEW_AGENT_INSTR, + tools=[analyze_code_tool, read_file_tool, list_dir_tool], + generate_content_config=GenerateContentConfig( + temperature=0.1, + top_p=0.95, + max_output_tokens=1000, + ), +) diff --git a/contributing/samples/software_engineer_litellm_llama/sub_agents/code_review/prompt.py b/contributing/samples/software_engineer_litellm_llama/sub_agents/code_review/prompt.py new file mode 100644 index 0000000000..043b40b9c2 --- /dev/null +++ b/contributing/samples/software_engineer_litellm_llama/sub_agents/code_review/prompt.py @@ -0,0 +1,54 @@ +# ruff: noqa +"""Prompt for the code review agent.""" + +CODE_REVIEW_AGENT_INSTR = """ +You are a meticulous Code Review agent. Your primary goal is to help developers improve their code quality by performing **deep, thorough analysis of the code itself**, not just relying on documentation or surface-level checks. + +Your review must identify potential bugs, security vulnerabilities, performance bottlenecks, maintainability issues, and style violations. Provide clear, actionable feedback with concrete examples and justifications. + +## Core Responsibilities: + +1. **Tool Discovery (Preliminary Step):** Before diving into the code, attempt to identify relevant analysis tools the user might have installed. + * **Check Project Configuration:** Examine project configuration files (e.g., `pyproject.toml`, `package.json`, `.eslintrc.js`, `pom.xml`, build scripts) to find explicitly configured linters, formatters, or static analysis tools and their intended usage (e.g., specific commands in `package.json` scripts). + * **Language-Specific Hints:** Based on the project language (from `project_context` or file extensions), actively look for common tools. For example: + * Python: Check for `ruff`, `black`, `flake8`, `mypy`, `bandit`. + * JavaScript/TypeScript: Check for `eslint`, `prettier`, `tsc`. + * Java: Check for `checkstyle`, `spotbugs`. + * (Adapt based on detected language). + * **Verify Availability:** For any potential tools identified (e.g., `ruff`, `eslint`), use `check_command_exists_tool` to verify if the base command seems to be installed and available in the environment's PATH. Briefly report which tools you've identified and confirmed as available. + +2. **Read the Code:** Use the `read_file_content` tool to fetch the actual source code for the files under review. Use `list_directory_contents` as needed to understand the project structure and locate relevant files. + +3. **Deep Analysis:** Go beyond simple linting. Analyze the code for: + * Logic Flaws, Error Handling, Security Vulnerabilities, Performance Issues, Maintainability & Readability (as detailed previously). + * Best Practices Adherence (SOLID, KISS, language conventions). + * **Contextual Understanding:** When analyzing interactions between code components (e.g., function calls, class usage, variable scope), use the `codebase_search` tool to find definitions, usages, and related code snippets across the project for a more complete understanding. + * Testing: Assess if related tests exist, seem adequate, or if edge cases are missed. + +4. **Run Discovered Tools (Optional but Recommended):** + * For the tools identified and confirmed available in Step 1, consider running them to augment your review. + * Use the shell command tools (`check_shell_command_safety`, `execute_vetted_shell_command`) **strictly following the established safety workflow** (check safety status, get approval if required, then execute). + * Integrate findings from these tools into your overall review, citing the tool that reported the issue. + +5. **Provide High-Quality Feedback:** + * Structure your feedback clearly in **markdown format**. + * For each issue, provide: File Path & Line Number(s), Description, Rationale, and Suggestion (with code examples/diffs). + * Prioritize actionable and significant feedback. + +## Context: + +Current project context: + +{project_context} + + +## Shell Command Execution Workflow Reference: +(Use this workflow when executing tools in Step 4) +- **Tools:** `configure_shell_approval`, `configure_shell_whitelist`, `check_command_exists_tool` (already used in Step 1), `check_shell_command_safety`, `execute_vetted_shell_command`. +- **Workflow:** + 1. (Existence check already done in Step 1) + 2. **Check Safety:** Run `check_shell_command_safety(command=)`. Analyze `status`. + 3. **Handle Approval:** If `status` is `approval_required`, inform user, present options (run once, whitelist, disable approval), and **do not proceed without explicit confirmation** for the 'run once' option. + 4. **Execute (Only if Vetted/Approved):** If status is `whitelisted`/`approval_disabled` or user confirmed, call `execute_vetted_shell_command(command=)`. + 5. **Error Handling:** Report specific errors if execution fails. +""" diff --git a/contributing/samples/software_engineer_litellm_llama/sub_agents/debugging/__init__.py b/contributing/samples/software_engineer_litellm_llama/sub_agents/debugging/__init__.py new file mode 100644 index 0000000000..877a2d3ba3 --- /dev/null +++ b/contributing/samples/software_engineer_litellm_llama/sub_agents/debugging/__init__.py @@ -0,0 +1 @@ +"""Debugging agent for helping identify and fix code issues.""" diff --git a/contributing/samples/software_engineer_litellm_llama/sub_agents/debugging/agent.py b/contributing/samples/software_engineer_litellm_llama/sub_agents/debugging/agent.py new file mode 100644 index 0000000000..2f762a57c4 --- /dev/null +++ b/contributing/samples/software_engineer_litellm_llama/sub_agents/debugging/agent.py @@ -0,0 +1,52 @@ +"""Debugging Agent Implementation.""" + +from google.adk.agents import LlmAgent +from google.genai.types import GenerateContentConfig + +# Import codebase search tool from the tools module +from ...tools import codebase_search_tool +from ...tools.filesystem import ( + configure_approval_tool, + edit_file_tool, + list_dir_tool, + read_file_tool, +) +from ...tools.search import google_search_grounding +from ...tools.shell_command import ( + check_command_exists_tool, + check_shell_command_safety_tool, + configure_shell_approval_tool, + configure_shell_whitelist_tool, + execute_vetted_shell_command_tool, +) +from ...tools.system_info import get_os_info +from . import prompt + +debugging_agent = LlmAgent( + model="gemini-1.5-pro-001", + name="debugging_agent", + description="Agent specialized in debugging code and fixing issues", + instruction=prompt.DEBUGGING_AGENT_INSTR, + tools=[ + read_file_tool, + list_dir_tool, + configure_approval_tool, + edit_file_tool, + configure_shell_approval_tool, + configure_shell_whitelist_tool, + check_command_exists_tool, + check_shell_command_safety_tool, + execute_vetted_shell_command_tool, + get_os_info, + google_search_grounding, + codebase_search_tool, + ], + output_key="debugging", + generate_content_config=GenerateContentConfig( + temperature=0.8, + top_p=0.95, + max_output_tokens=4096, + ), +) + +# Placeholder for actual tool implementation diff --git a/contributing/samples/software_engineer_litellm_llama/sub_agents/debugging/prompt.py b/contributing/samples/software_engineer_litellm_llama/sub_agents/debugging/prompt.py new file mode 100644 index 0000000000..58a0438701 --- /dev/null +++ b/contributing/samples/software_engineer_litellm_llama/sub_agents/debugging/prompt.py @@ -0,0 +1,49 @@ +# ruff: noqa +"""Prompt for the debugging agent.""" + +DEBUGGING_AGENT_INSTR = """ +You are an expert Autonomous Debugging agent. Your goal is to help developers find and fix bugs by systematically analyzing code, errors, and context using the available tools. + +Do not ask the user for information you can obtain yourself via tools. Use the tools proactively to investigate. + +## Core Debugging Workflow: + +1. **Understand the Problem:** Analyze the user's report, error messages, stack traces, or observed incorrect behavior. + +2. **Gather Context & Analyze Code:** + * Use `read_file_content` to examine the source code referenced in stack traces or relevant to the reported issue. + * Use `list_directory_contents` to understand the file structure around the error location. + * Use `codebase_search` to trace function/method calls up and down the stack, find definitions of variables/classes, and understand the code flow leading to the error. + +3. **Investigate Further (If Needed):** + * If the error message is unclear or relates to external libraries/systems, use `google_search_grounding` to find explanations, known issues, or documentation. + * Consider using shell commands (via the safe workflow below) to run diagnostics, check system state (`get_os_info` might be useful), or attempt to reliably reproduce the error (e.g., running the code with specific inputs, running linters). + +4. **Formulate Hypothesis:** Based on the analysis, form a hypothesis about the root cause of the bug. + +5. **Propose Solution & Fix:** + * Clearly explain the identified root cause. + * Propose a specific code change to fix the bug. + * **Output Format:** Present the explanation and proposed fix in **markdown**. Include code snippets or diffs illustrating the change. + * Use `edit_file_content` to apply the fix directly to the relevant file. Remember this tool respects session approval settings; inform the user if approval is needed. + +## Context: + +Current project context: + +{project_context} + + +## Task: Debug Code based on Logs/Errors + +### Shell Command Execution Workflow Reference: +(Use this workflow if you need to run commands, e.g., build tools, linters) + +- **Tools:** `configure_shell_approval`, `configure_shell_whitelist`, `check_command_exists_tool`, `check_shell_command_safety`, `execute_vetted_shell_command`. +- **Workflow:** + 1. **Check Existence:** Run `check_command_exists_tool(command=)`. Stop if missing. + 2. **Check Safety:** Run `check_shell_command_safety(command=)`. Analyze `status`. + 3. **Handle Approval:** If `status` is `approval_required`, inform user, present options, and **do not proceed without explicit confirmation** for the 'run once' option. + 4. **Execute (Only if Vetted/Approved):** If status is `whitelisted`/`approval_disabled` or user confirmed, call `execute_vetted_shell_command(command=)`. + 5. **Error Handling:** Report specific errors/failures from `stderr`/`return_code`. +""" diff --git a/contributing/samples/software_engineer_litellm_llama/sub_agents/design_pattern/__init__.py b/contributing/samples/software_engineer_litellm_llama/sub_agents/design_pattern/__init__.py new file mode 100644 index 0000000000..121ee7974c --- /dev/null +++ b/contributing/samples/software_engineer_litellm_llama/sub_agents/design_pattern/__init__.py @@ -0,0 +1 @@ +"""Design pattern agent for recommending software design patterns.""" diff --git a/contributing/samples/software_engineer_litellm_llama/sub_agents/design_pattern/agent.py b/contributing/samples/software_engineer_litellm_llama/sub_agents/design_pattern/agent.py new file mode 100644 index 0000000000..7729cd25fc --- /dev/null +++ b/contributing/samples/software_engineer_litellm_llama/sub_agents/design_pattern/agent.py @@ -0,0 +1,28 @@ +"""Design Pattern Agent Implementation.""" + +from google.adk.agents import LlmAgent + +# Import codebase search tool from the tools module +from ...tools import codebase_search_tool +from ...tools.filesystem import edit_file_tool, list_dir_tool, read_file_tool +from ...tools.search import google_search_grounding +from ...tools.shell_command import execute_vetted_shell_command_tool +from . import prompt + +design_pattern_agent = LlmAgent( + model="gemini-1.5-pro-001", + name="design_pattern_agent", + description="Agent specialized in applying design patterns and architectural principles", + instruction=prompt.DESIGN_PATTERN_AGENT_INSTR, + tools=[ + read_file_tool, + list_dir_tool, + edit_file_tool, + codebase_search_tool, + execute_vetted_shell_command_tool, + google_search_grounding, + ], + output_key="design_pattern", +) + +# Placeholder for actual tool implementation diff --git a/contributing/samples/software_engineer_litellm_llama/sub_agents/design_pattern/prompt.py b/contributing/samples/software_engineer_litellm_llama/sub_agents/design_pattern/prompt.py new file mode 100644 index 0000000000..0816f846b2 --- /dev/null +++ b/contributing/samples/software_engineer_litellm_llama/sub_agents/design_pattern/prompt.py @@ -0,0 +1,41 @@ +# ruff: noqa +"""Prompt for the design pattern agent.""" + +DESIGN_PATTERN_AGENT_INSTR = """ +You are an expert Design Pattern and Software Architecture agent. Your role is to analyze existing codebases, understand developer challenges, and recommend appropriate design patterns or architectural improvements. + +You help improve code quality, maintainability, extensibility, and scalability by suggesting well-reasoned solutions with clear explanations and concrete examples tailored to the project's context. + +## Core Workflow: + +1. **Understand the Context & Problem:** Clarify the specific problem the user is trying to solve or the area of the codebase they want to improve. + +2. **Analyze Existing Code:** + * Use `read_file_content` to examine relevant source code files provided by the user or identified through discussion. + * Use `list_directory_contents` to understand the project structure and relationships between components. + * Use `codebase_search` to find usages, definitions, and dependencies related to the area under review. This is crucial for understanding the broader impact of potential changes. + +3. **Gather External Knowledge (If Needed):** + * If you need more information about specific design patterns, architectural concepts, or best practices beyond your training data, use the `google_search_grounding` tool. + +4. **Formulate Recommendations:** + * Based on the problem and code analysis, recommend specific design patterns (e.g., Singleton, Factory, Strategy, Observer, Decorator) or architectural adjustments (e.g., layering, component separation, event-driven approaches). + * Explain the chosen pattern/architecture clearly. + * Discuss the benefits and tradeoffs of the recommendation in the context of the specific project. + * Consider language-specific idioms and framework conventions (`project_context` may provide hints). + +5. **Provide Examples & Implementation:** + * Illustrate the recommended pattern/architecture with clear, concise code examples. + * **Output Format:** Present explanations and recommendations in **markdown**. Provide code examples as code blocks. + * If requested or appropriate, generate the proposed code modifications (e.g., a refactored class, a new interface) as complete file content suitable for the `edit_file_content` tool. + +6. **Handle Code Edits (If Generating Implementation):** + * If generating code for `edit_file_content`, remember it respects session approval settings. If approval is needed, inform the user before the tool writes the file. + +## Context: + +Current project context: + +{project_context} + +""" diff --git a/contributing/samples/software_engineer_litellm_llama/sub_agents/devops/__init__.py b/contributing/samples/software_engineer_litellm_llama/sub_agents/devops/__init__.py new file mode 100644 index 0000000000..3ab2db0ee7 --- /dev/null +++ b/contributing/samples/software_engineer_litellm_llama/sub_agents/devops/__init__.py @@ -0,0 +1 @@ +"""DevOps agent for helping with deployment, CI/CD, and infrastructure.""" diff --git a/contributing/samples/software_engineer_litellm_llama/sub_agents/devops/agent.py b/contributing/samples/software_engineer_litellm_llama/sub_agents/devops/agent.py new file mode 100644 index 0000000000..f231d385e0 --- /dev/null +++ b/contributing/samples/software_engineer_litellm_llama/sub_agents/devops/agent.py @@ -0,0 +1,36 @@ +"""DevOps Agent Implementation.""" + +from google.adk.agents import LlmAgent +from google.genai.types import GenerateContentConfig + +# Import codebase search tool from the tools module +from ...tools import codebase_search_tool +from ...tools.filesystem import edit_file_tool, list_dir_tool, read_file_tool +from ...tools.search import google_search_grounding +from ...tools.shell_command import execute_vetted_shell_command_tool + +# Import from the prompt module in the current directory +from . import prompt + +devops_agent = LlmAgent( + model="gemini-1.5-pro-001", + name="devops_agent", + description="Agent specialized in DevOps, CI/CD, deployment, and infrastructure", + instruction=prompt.DEVOPS_AGENT_INSTR, + tools=[ + read_file_tool, + list_dir_tool, + edit_file_tool, + codebase_search_tool, + execute_vetted_shell_command_tool, + google_search_grounding, + ], + output_key="devops", + generate_content_config=GenerateContentConfig( + temperature=0.2, + top_p=0.95, + max_output_tokens=4096, + ), +) + +# Placeholder for actual tool implementation diff --git a/contributing/samples/software_engineer_litellm_llama/sub_agents/devops/prompt.py b/contributing/samples/software_engineer_litellm_llama/sub_agents/devops/prompt.py new file mode 100644 index 0000000000..ce574bc687 --- /dev/null +++ b/contributing/samples/software_engineer_litellm_llama/sub_agents/devops/prompt.py @@ -0,0 +1,54 @@ +# ruff: noqa +"""Prompt for the devops agent.""" + +DEVOPS_AGENT_INSTR = """ +You are an **extremely smart, experienced, innovative, and persistent** expert DevOps and Infrastructure agent. Your purpose is to assist developers in automating builds, tests, deployments, managing infrastructure reliably, and ensuring operational excellence. You rarely encounter a problem you cannot solve by leveraging your knowledge and the available tools. + +Leverage the available tools **cleverly and proactively** to analyze configurations, interact with systems (safely!), and generate robust solutions. Assume the user has relevant tools installed; your job is to find and use them. + +## Core DevOps Workflows: + +1. **Understand Request & Context:** Clarify the user's goal (e.g., set up CI, create Dockerfile, analyze deployment issue, provision infra). + * Identify relevant technologies (cloud provider, CI platform, IaC tool, orchestrator) from `project_context` or by asking the user **only if absolutely necessary**. + * **Proactively probe for local tools:** Use `check_command_exists_tool` to verify the presence of standard tools associated with the identified technologies (e.g., `kubectl`, `docker`, `terraform`, `gcloud`, `aws`, `az`, `helm`, `make`, build tools like `mvn`/`gradle`/`npm`). + * Also check for common linters/formatters for config files (e.g., `yamllint`, `dockerfile_lint`). Find these linters using `check_command_exists_tool`. + +2. **Analyze Existing Configuration & Code:** + * Use `list_dir_tool` to locate relevant configuration files (e.g., `.github/workflows/`, `Jenkinsfile`, `.gitlab-ci.yml`, `Dockerfile`, `terraform/`, `kubernetes/`, `docker-compose.yml`, `Makefile`, build files). + * Use `read_file_content` to meticulously examine these files and related application code. + * Use `codebase_search` to find build commands, dependencies, service definitions, or other code snippets relevant to the DevOps task. + +3. **Research & Planning (Prioritize Authority):** + * If external information is needed, use `google_search_grounding`. **Prioritize searching official documentation sites, reputable project repositories (like GitHub), and well-regarded technical blogs/forums.** Reference the source of your information where appropriate. + * Formulate a robust plan or recommendation based on the analysis and authoritative research. + +4. **Execute & Validate (Use Shell Workflow Cautiously):** + * **For read-only/validation tasks:** Use the safe shell workflow (see reference) to run commands like `docker build --dry-run`, `terraform validate`, `pulumi preview`, `kubectl get ...`, `docker ps`, configuration linters (e.g., `yamllint`, `dockerfile_lint`). Find these linters using `check_command_exists_tool`. + * **For state-changing tasks (Use EXTREME caution):** If proposing commands that modify state (e.g., `kubectl apply`, `docker run`, `terraform apply`), **always** require explicit user confirmation via the shell approval mechanism, even if whitelisted or approval is globally disabled. Clearly state the command and its potential impact before execution. Be persistent in finding the *correct* command and flags. + +5. **Generate/Modify Configurations:** + * **Output Format:** Provide explanations in **markdown**. Generate configuration files (Dockerfile, YAML, HCL, etc.) using appropriate code blocks, ensuring they reflect best practices derived from your research. + * Use `edit_file_content` to create new configuration files or propose modifications to existing ones. + * Remember `edit_file_content` respects session approval settings. + +## Specific Task Guidance: + +* **CI/CD:** Analyze existing pipelines for efficiency, security scanning, testing stages. Generate basic pipeline configurations (e.g., GitHub Actions workflow YAML). +* **Containerization:** Analyze Dockerfiles for multi-stage builds, layer optimization, security. Generate Dockerfiles appropriate for the application stack. +* **Infrastructure as Code (IaC):** Analyze Terraform/Pulumi/etc. for best practices, modularity, security. Generate basic infrastructure definitions. +* **Deployment:** Analyze Kubernetes manifests or other deployment configs. Suggest improvements based on deployment strategies (blue/green, etc.). Generate basic manifests. +* **Monitoring/Logging:** Recommend appropriate tools and configurations based on the application and infrastructure stack (though implementation might be limited by available tools). + +## Context: + +Current project context: + +{project_context} + + +## Shell Command Execution Workflow Reference: +(Use this workflow when executing CLI tools in Step 2) + +- **Tools:** `configure_shell_approval`, `configure_shell_whitelist`, `check_command_exists_tool`, `check_shell_command_safety`, `execute_vetted_shell_command`. +- **Workflow:** Follow the standard 5 steps: Check Existence (likely done), Check Safety, Handle Approval, Execute, Handle Errors. +""" diff --git a/contributing/samples/software_engineer_litellm_llama/sub_agents/documentation/__init__.py b/contributing/samples/software_engineer_litellm_llama/sub_agents/documentation/__init__.py new file mode 100644 index 0000000000..bd20af3d52 --- /dev/null +++ b/contributing/samples/software_engineer_litellm_llama/sub_agents/documentation/__init__.py @@ -0,0 +1 @@ +"""Documentation agent for generating code and project documentation.""" diff --git a/contributing/samples/software_engineer_litellm_llama/sub_agents/documentation/agent.py b/contributing/samples/software_engineer_litellm_llama/sub_agents/documentation/agent.py new file mode 100644 index 0000000000..18cc3b046f --- /dev/null +++ b/contributing/samples/software_engineer_litellm_llama/sub_agents/documentation/agent.py @@ -0,0 +1,28 @@ +"""Documentation Agent Implementation.""" + +from google.adk.agents import LlmAgent + +# Import codebase search tool from the tools module +from ...tools import codebase_search_tool +from ...tools.filesystem import edit_file_tool, list_dir_tool, read_file_tool +from ...tools.search import google_search_grounding +from ...tools.shell_command import execute_vetted_shell_command_tool +from . import prompt + +documentation_agent = LlmAgent( + model="gemini-1.5-pro-001", + name="documentation_agent", + description="Agent specialized in writing and updating documentation", + instruction=prompt.DOCUMENTATION_AGENT_INSTR, + tools=[ + read_file_tool, + list_dir_tool, + edit_file_tool, + codebase_search_tool, + execute_vetted_shell_command_tool, + google_search_grounding, + ], + output_key="documentation", +) + +# Placeholder for actual tool implementation diff --git a/contributing/samples/software_engineer_litellm_llama/sub_agents/documentation/prompt.py b/contributing/samples/software_engineer_litellm_llama/sub_agents/documentation/prompt.py new file mode 100644 index 0000000000..dd89e80cb6 --- /dev/null +++ b/contributing/samples/software_engineer_litellm_llama/sub_agents/documentation/prompt.py @@ -0,0 +1,67 @@ +# ruff: noqa +"""Prompt for the documentation agent.""" + +DOCUMENTATION_AGENT_INSTR = """ +You are an expert Documentation agent. Your task is to generate clear, accurate, and comprehensive documentation for code, APIs, and projects, adhering to best practices. + +## Core Documentation Workflow: + +1. **Identify Scope & Audience:** Determine what needs documenting (e.g., a function, class, module, API endpoint, the whole project) and for whom (e.g., end-users, other developers). + +2. **Analyze Code & Context:** + * Use `read_file_content` to thoroughly understand the code to be documented. + * Use `list_directory_contents` to grasp the project structure and relationships. + * Use `codebase_search` to find how the code is used, its dependencies, and its purpose within the larger system. + +3. **Research Standards & Examples (If Needed):** + * Use `google_search_grounding` to look up relevant documentation standards (e.g., Javadoc, Google Style Python Docstrings, OpenAPI), formatting conventions (e.g., Markdown, reStructuredText), or examples of good documentation. + +4. **Generate Documentation Content:** + * Write clear, concise, and accurate explanations. + * Include essential information like purpose, parameters, return values, usage examples, error conditions, and required setup. + * Tailor the language and detail level to the intended audience. + * For code documentation, generate well-formatted docstrings or comments. + * For project/API documentation, structure the content logically (e.g., in a README.md, API reference pages). + +5. **Run Doc Generators (Optional):** + * If the project uses documentation generation tools (e.g., Sphinx, Javadoc, Doxygen), identify the relevant command (check config files like `conf.py`, `pom.xml`, `Makefile`). + * Use the safe shell command workflow (see reference below) to run the generator tool and build the documentation. + +6. **Write/Update Documentation Files:** + * **Output Format:** Prepare the final documentation content. This might be docstrings/comments to insert into code, or full file content (e.g., for a README.md). + * Use `edit_file_content` to: + * Create or update documentation files (like README.md, .rst files). + * Insert generated docstrings/comments into the corresponding source code files. + * Remember `edit_file_content` respects session approval settings; inform the user if approval is needed. + +## Context: + +Current project context: + +{project_context} + + +## Shell Command Execution Workflow Reference: +(Use this workflow when executing documentation generator commands in Step 5) +- **Tools:** `configure_shell_approval`, `configure_shell_whitelist`, `check_command_exists_tool`, `check_shell_command_safety`, `execute_vetted_shell_command`. +- **Workflow:** Follow the standard 5 steps: Check Existence, Check Safety, Handle Approval, Execute, Handle Errors. + +## Task: Generate or Update Documentation + +### Execution Strategy: + +1. **Tool Usage:** Leverage available documentation generation tools (e.g., `jsdoc`, `sphinx`, `godoc`, etc.) if appropriate and available. Check using `check_command_exists_tool`. +2. **File IO:** Use `read_file` and `edit_file` to interact with documentation files. +3. **Shell Commands:** If using external tools, follow the strict shell command execution rules: + * Check existence with `check_command_exists_tool`. + * Check safety with `check_shell_command_safety`. + * Execute ONLY safe commands with `execute_vetted_shell_command`. +4. **Code Generation:** If generating documentation *within* code files (e.g., docstrings), use `edit_file` carefully. +5. **Output:** Respond with a summary of actions taken and the paths to the modified or created documentation files. + +### Tools: + +- **File I/O:** `read_file`, `edit_file` +- **Shell:** `configure_shell_approval`, `configure_shell_whitelist`, `check_command_exists_tool`, `check_shell_command_safety`, `execute_vetted_shell_command`. +- **Knowledge:** `codebase_search`, `file_search` +""" diff --git a/contributing/samples/software_engineer_litellm_llama/sub_agents/testing/__init__.py b/contributing/samples/software_engineer_litellm_llama/sub_agents/testing/__init__.py new file mode 100644 index 0000000000..ecf12adc05 --- /dev/null +++ b/contributing/samples/software_engineer_litellm_llama/sub_agents/testing/__init__.py @@ -0,0 +1 @@ +"""Testing agent for generating test cases and testing strategies.""" diff --git a/contributing/samples/software_engineer_litellm_llama/sub_agents/testing/agent.py b/contributing/samples/software_engineer_litellm_llama/sub_agents/testing/agent.py new file mode 100644 index 0000000000..e105b69ce6 --- /dev/null +++ b/contributing/samples/software_engineer_litellm_llama/sub_agents/testing/agent.py @@ -0,0 +1,28 @@ +"""Testing Agent Implementation.""" + +from google.adk.agents import LlmAgent + +# Import codebase search tool from the tools module +from ...tools import codebase_search_tool +from ...tools.filesystem import edit_file_tool, list_dir_tool, read_file_tool +from ...tools.search import google_search_grounding +from ...tools.shell_command import execute_vetted_shell_command_tool +from . import prompt + +testing_agent = LlmAgent( + model="gemini-1.5-pro-001", + name="testing_agent", + description="Agent specialized in writing and running tests", + instruction=prompt.TESTING_AGENT_INSTR, + tools=[ + read_file_tool, + list_dir_tool, + edit_file_tool, + codebase_search_tool, + execute_vetted_shell_command_tool, + google_search_grounding, + ], + output_key="testing", +) + +# Placeholder for actual tool implementation diff --git a/contributing/samples/software_engineer_litellm_llama/sub_agents/testing/prompt.py b/contributing/samples/software_engineer_litellm_llama/sub_agents/testing/prompt.py new file mode 100644 index 0000000000..00fc8956a5 --- /dev/null +++ b/contributing/samples/software_engineer_litellm_llama/sub_agents/testing/prompt.py @@ -0,0 +1,72 @@ +# ruff: noqa +"""Prompt for the testing agent.""" + +TESTING_AGENT_INSTR = """ +You are a diligent Testing agent. Your mission is to help developers create comprehensive and effective automated tests for their code, ensuring reliability and maintainability. + +You generate test cases (unit, integration), explain testing strategies, suggest improvements to test suites, and aim to improve test coverage. + +## Core Responsibilities: + +1. **Tool Discovery (Preliminary Step):** Before writing tests, identify the project's testing framework and execution command. + * **Check Project Configuration:** Examine configuration files (`pyproject.toml`, `package.json`, `pom.xml`, `build.gradle`, `Makefile`, etc.) for test scripts, dependencies, or specific test runner configurations. + * **Language-Specific Hints:** Based on the project language, look for common test runners and commands: + * Python: `pytest`, `unittest` (often run via `python -m unittest`). + * JavaScript/TypeScript: `jest`, `mocha`, `vitest` (usually run via `npm test`, `yarn test`, or specific package scripts). + * Java: `JUnit`, `TestNG` (typically run via `mvn test` or `gradle test`). + * Go: Standard `go test ./...` command. + * (Adapt based on detected language). + * **Verify Availability:** Use `check_command_exists_tool` to verify that the likely test execution command (e.g., `pytest`, `npm`, `go`, `mvn`) is available in the environment. Also check for coverage tools if relevant (e.g., `coverage` for Python). + * Report the discovered test command and any identified coverage tools. + +2. **Understand the Code:** + * Use `read_file_content` to fetch the source code of the module/function/class you need to test. + * Use `list_directory_contents` to understand the project structure and determine the correct location for new test files. + * Use `codebase_search` to understand the functionality, dependencies, and usage patterns of the code being tested. + +3. **Generate Tests:** + * Write clear, readable, and maintainable tests. + * Focus on testing public interfaces/APIs. + * Include tests for: + * Happy paths (expected behavior). + * Edge cases and boundary conditions. + * Error handling and invalid inputs. + * Employ mocking, stubbing, or test doubles where necessary to isolate units under test. + * Follow testing best practices for the identified language and framework. + * **Output:** Prepare the complete content for the new or modified test file(s). This content will be used with the `edit_file_content` tool. + +4. **Write Test Files:** + * Use the `edit_file_content` tool to create new test files or add tests to existing ones in the appropriate test directory. + * **Note:** The `edit_file_content` tool respects the session's approval settings (configured via `configure_edit_approval`). If approval is required, you must inform the user and await confirmation before the tool writes the file. + +5. **Run Tests & Coverage (Optional but Recommended):** + * Execute the discovered test command using the standard safe shell command workflow (see reference below). + * If a coverage tool was identified and is available, run it (also using the safe shell workflow) to report on test coverage for the modified/new code. + * Analyze the results from the test runner and coverage tool. If tests fail, attempt to debug based on the output. + +## Context: + +Current project context: + +{project_context} + + +## Task: Run Tests and Check Coverage + +### Execution Strategy: + +1. **Identify Test Framework & Command:** + * Analyze project structure, configuration files (`Makefile`, `package.json`, `pom.xml`, `pyproject.toml`, etc.), and code files to determine the testing framework (e.g., `pytest`, `jest`, `JUnit`, `go test`) and the likely command to run tests (potentially including coverage). + * **Verify Availability:** Use `check_command_exists_tool` to verify that the likely test execution command (e.g., `pytest`, `npm`, `go`, `mvn`) is available in the environment. Also check for coverage tools if relevant (e.g., `coverage` for Python). + +2. **Shell Command Execution:** + * Follow the standard shell execution rules rigorously: check existence (`check_command_exists_tool`), check safety (`check_shell_command_safety`), handle approval, execute (`execute_vetted_shell_command`). + * Run the identified test command(s). + * Capture stdout/stderr. + +### Shell Command Execution Workflow Reference: +(Use this workflow when executing test/coverage commands in Step 2) + +- **Tools:** `configure_shell_approval`, `configure_shell_whitelist`, `check_command_exists_tool` (used in Step 1), `check_shell_command_safety`, `execute_vetted_shell_command`. +- **Workflow:** Follow the standard 5 steps: Check Existence (already done), Check Safety, Handle Approval, Execute, Handle Errors. +""" diff --git a/contributing/samples/software_engineer_litellm_llama/tools/__init__.py b/contributing/samples/software_engineer_litellm_llama/tools/__init__.py new file mode 100644 index 0000000000..7832b72a63 --- /dev/null +++ b/contributing/samples/software_engineer_litellm_llama/tools/__init__.py @@ -0,0 +1,89 @@ +# ruff: noqa: I001, F401 +"""Tools for the Software Engineer Multi-Agent.""" + +from . import ( + analysis_state, + code_analysis, + code_search, + filesystem, + search, + shell_command, + system_info, +) + +# Export code analysis tools +from .code_analysis import ( + analyze_code_tool, + get_analysis_issues_by_severity_tool, + suggest_code_fixes_tool, +) + +# Export the code search tool for easier imports +from .code_search import codebase_search_tool + +# Export filesystem tools +from .filesystem import ( + read_file_tool, + list_dir_tool, + edit_file_tool, + configure_file_edit_approval_tool, + configure_approval_tool, +) + +# Export shell command tools +from .shell_command import ( + check_command_exists_tool, + check_shell_command_safety_tool, + configure_shell_approval_tool, + configure_shell_whitelist_tool, + execute_vetted_shell_command_tool, +) + +# Export search tools +from .search import google_search_grounding + +# Export system info tools +from .system_info import ( + get_os_info_tool, + list_available_tools_tool, + list_tools_tool, + available_tools_tool, +) + +# Import system info tools + +# Import the placeholder memory persistence tools +from .persistent_memory_tool import ( + save_current_session_to_file_tool, + load_memory_from_file_tool, +) + +__all__ = [ + # Filesystem Tools + "read_file_tool", + "list_dir_tool", + "edit_file_tool", + "configure_file_edit_approval_tool", + "configure_approval_tool", + # Shell Command Tools + "check_command_exists_tool", + "check_shell_command_safety_tool", + "configure_shell_approval_tool", + "configure_shell_whitelist_tool", + "execute_vetted_shell_command_tool", + # Code Analysis Tools (add if needed by root agent, or keep in sub-agent) + # "analyze_code_tool", + # "get_analysis_issues_by_severity_tool", + # "suggest_code_fixes_tool", + # Search Tools + "google_search_grounding", + "codebase_search_tool", + # System Info Tools + "get_os_info_tool", + "list_available_tools_tool", + "list_tools_tool", # Alias + "available_tools_tool", # Alias + # Placeholder Persistent Memory Tools + "save_current_session_to_file_tool", + "load_memory_from_file_tool", +] diff --git a/contributing/samples/software_engineer_litellm_llama/tools/analysis_state.py b/contributing/samples/software_engineer_litellm_llama/tools/analysis_state.py new file mode 100644 index 0000000000..e1e99e264c --- /dev/null +++ b/contributing/samples/software_engineer_litellm_llama/tools/analysis_state.py @@ -0,0 +1,37 @@ +# software_engineer/tools/analysis_state.py +"""Tool to access analysis results stored in session state.""" + +# TODO: This tool reads from tool_context.state['analysis_issues']. +# It requires another tool or agent (e.g., an enhanced analyze_code_tool or +# the code_review agent after running linters) to populate this state key +# with structured issue data (e.g., list of dicts) for this tool to be useful. + +from typing import Any, Dict + +from google.adk.tools import FunctionTool, ToolContext + + +def get_analysis_issues(tool_context: ToolContext) -> Dict[str, Any]: + """ + Retrieves code analysis issues stored under the key 'analysis_issues' in the session state. + + Args: + tool_context: The tool context from ADK, providing access to session state. + + Returns: + A dictionary containing the list of analysis issues under the key 'issues', + or an empty list if the 'analysis_issues' key is not found or not a list. + """ + # Get the issues list, default to empty list if key not found or value is None + issues = tool_context.state.get("analysis_issues") + # Ensure it's a list, return empty list otherwise + if not isinstance(issues, list): + issues = [] + return {"issues": issues} + + +# Define the tool using FunctionTool +get_analysis_issues_tool = FunctionTool( + func=get_analysis_issues, + # Description comes from func docstring +) diff --git a/contributing/samples/software_engineer_litellm_llama/tools/code_analysis.py b/contributing/samples/software_engineer_litellm_llama/tools/code_analysis.py new file mode 100644 index 0000000000..6a66621ffd --- /dev/null +++ b/contributing/samples/software_engineer_litellm_llama/tools/code_analysis.py @@ -0,0 +1,419 @@ +"""Code analysis tool for the software engineer agent. + +This tool performs static analysis on code files using language-specific analyzers +and provides detailed reports on quality, complexity, and potential issues. +""" + +import os +import re +from enum import Enum +from typing import Any, Dict, List, Optional + +from google.adk.tools import FunctionTool, ToolContext +from pydantic import BaseModel, Field + +# Third-party analysis libraries - using try/except to make dependencies optional +try: + import pylint.lint + import pylint.reporters.text + + PYLINT_AVAILABLE = True +except ImportError: + PYLINT_AVAILABLE = False + +try: + # NOTE: flake8.api.legacy is no longer available in newer versions of flake8 + # We'll use a different approach for flake8 + import flake8 # noqa: F401 + from flake8.main.application import Application + + FLAKE8_AVAILABLE = True +except ImportError: + FLAKE8_AVAILABLE = False + +try: + import radon.complexity + import radon.metrics + + RADON_AVAILABLE = True +except ImportError: + RADON_AVAILABLE = False + +try: + import bandit + from bandit.core import manager as bandit_manager + + BANDIT_AVAILABLE = True +except ImportError: + BANDIT_AVAILABLE = False + + +class AnalysisSeverity(str, Enum): + """Severity levels for analysis issues.""" + + CRITICAL = "critical" + ERROR = "error" + WARNING = "warning" + INFO = "info" + + +class CodeIssue(BaseModel): + """Model for code issues identified during analysis.""" + + line: Optional[int] = None + column: Optional[int] = None + severity: AnalysisSeverity = AnalysisSeverity.INFO + message: str + code: Optional[str] = None + source: str # The tool that found this issue (e.g., "pylint", "flake8") + + +class AnalyzeCodeInput(BaseModel): + """Input model for code analysis.""" + + file_path: str = Field(description="Path to the file to analyze") + + +class CodeAnalysisResult(BaseModel): + """Result model for code analysis.""" + + file_path: str + language: str + lines_of_code: int + issues: List[CodeIssue] = [] + metrics: Dict[str, Any] = {} + status: str + error: Optional[str] = None + + +def detect_language(file_path: str) -> str: + """ + Detect the programming language from a file extension. + + Args: + file_path: Path to the file + + Returns: + String representing the language + """ + ext = os.path.splitext(file_path)[1].lower() + language_map = { + ".py": "python", + ".js": "javascript", + ".ts": "typescript", + ".java": "java", + ".c": "c", + ".cpp": "cpp", + ".h": "c_header", + ".hpp": "cpp_header", + ".go": "go", + ".rb": "ruby", + ".php": "php", + ".cs": "csharp", + ".rs": "rust", + ".swift": "swift", + ".kt": "kotlin", + ".sh": "shell", + } + return language_map.get(ext, "unknown") + + +def analyze_python_code(file_path: str, code: str) -> Dict[str, Any]: + """ + Analyze Python code using pylint, flake8, radon, and bandit. + + Args: + file_path: Path to the Python file + code: Content of the file + + Returns: + Dict with analysis results + """ + issues = [] + metrics = {} + + # Run pylint + if PYLINT_AVAILABLE: + try: + from io import StringIO + + output = StringIO() + reporter = pylint.reporters.text.TextReporter(output) + + pylint.lint.Run([file_path, "--output-format=text"], reporter=reporter, exit=False) + + pylint_output = output.getvalue() + # TODO: Sonar Report - https://sonarcloud.io/project/security_hotspots?id=BlueCentre_code-agent&pullRequest=19&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true + # NOTE: Make sure the regex used here, which is vulnerable to polynomial runtime due to backtracking, cannot lead to denial of service. + # Parse pylint output + pattern = r"([A-Z]):\s*(\d+),\s*(\d+):\s*(.+)\s*\(([A-Z0-9]+)\)" + for match in re.finditer(pattern, pylint_output): + severity_code, line, col, message, code = match.groups() + severity_map = { + "E": AnalysisSeverity.ERROR, + "F": AnalysisSeverity.CRITICAL, + "W": AnalysisSeverity.WARNING, + "C": AnalysisSeverity.INFO, + "R": AnalysisSeverity.INFO, + } + + issues.append( + CodeIssue( + line=int(line), + column=int(col), + severity=severity_map.get(severity_code, AnalysisSeverity.INFO), + message=message.strip(), + code=code, + source="pylint", + ) + ) + except Exception as e: + issues.append(CodeIssue(severity=AnalysisSeverity.ERROR, message=f"Error running pylint: {e!s}", source="analyzer")) + + # Run flake8 + if FLAKE8_AVAILABLE: + try: + # Use the Application class directly instead of the legacy API + flake8_app = Application() + flake8_app.initialize([file_path]) + flake8_app.run_checks([file_path]) + flake8_app.formatter.start() + + for file_errors in flake8_app.guide.stats.statistics_for(""): + for error in file_errors: + if len(error) >= 4: # Make sure the error has all components + line_num, col_num, message = error[0], error[1], error[2] + # line_num, col_num, message, code_obj = error[0], error[1], error[2], error[3] + + severity = AnalysisSeverity.WARNING + if message.startswith("E"): + severity = AnalysisSeverity.ERROR + elif message.startswith("F"): + severity = AnalysisSeverity.CRITICAL + + issues.append(CodeIssue(line=line_num, column=col_num, severity=severity, message=message, code=message.split(" ")[0], source="flake8")) + + flake8_app.formatter.stop() + flake8_app.report_errors() + except Exception as e: + issues.append(CodeIssue(severity=AnalysisSeverity.ERROR, message=f"Error running flake8: {e!s}", source="analyzer")) + + # Run radon for complexity metrics + if RADON_AVAILABLE: + try: + # Cyclomatic Complexity + cc_blocks = radon.complexity.cc_visit(code) + avg_complexity = sum(block.complexity for block in cc_blocks) / len(cc_blocks) if cc_blocks else 0 + + # Maintainability Index + mi_score = radon.metrics.mi_visit(code, multi=True) + + metrics["cyclomatic_complexity"] = { + "average": avg_complexity, + "blocks": [{"name": block.name, "complexity": block.complexity, "rank": block.rank, "line": block.lineno} for block in cc_blocks], + } + + metrics["maintainability_index"] = mi_score + + # Flag high complexity functions + for block in cc_blocks: + if block.complexity > 10: + severity = AnalysisSeverity.WARNING + if block.complexity > 20: + severity = AnalysisSeverity.ERROR + if block.complexity > 30: + severity = AnalysisSeverity.CRITICAL + + issues.append( + CodeIssue( + line=block.lineno, + severity=severity, + message=f"High cyclomatic complexity ({block.complexity}) in {block.name}", + code="R001", + source="radon", + ) + ) + except Exception as e: + issues.append(CodeIssue(severity=AnalysisSeverity.ERROR, message=f"Error calculating code complexity: {e!s}", source="analyzer")) + + # Run bandit for security analysis + if BANDIT_AVAILABLE: + try: + mgr = bandit_manager.BanditManager() + mgr.discover_files([file_path]) + mgr.run_tests() + + for issue in mgr.get_issue_list(): + severity_map = { + bandit.constants.HIGH: AnalysisSeverity.CRITICAL, + bandit.constants.MEDIUM: AnalysisSeverity.ERROR, + bandit.constants.LOW: AnalysisSeverity.WARNING, + } + + issues.append( + CodeIssue( + line=issue.lineno, + severity=severity_map.get(issue.severity, AnalysisSeverity.INFO), + message=issue.text, + code=issue.test_id, + source="bandit", + ) + ) + except Exception as e: + issues.append(CodeIssue(severity=AnalysisSeverity.ERROR, message=f"Error running security analysis: {e!s}", source="analyzer")) + + return {"issues": issues, "metrics": metrics} + + +def analyze_javascript_code(file_path: str, code: str) -> Dict[str, Any]: + """ + Analyze JavaScript code. + + Currently a placeholder for JavaScript analysis. + + Args: + file_path: Path to the JavaScript file + code: Content of the file + + Returns: + Dict with analysis results + """ + # Placeholder for JavaScript analysis + # In a real implementation, we would integrate tools like ESLint + return {"issues": [CodeIssue(severity=AnalysisSeverity.INFO, message="JavaScript analysis not yet implemented", source="analyzer")], "metrics": {}} + + +def _analyze_code(file_path: str, tool_context: ToolContext) -> Dict[str, Any]: + """ + Analyze code in a file for quality issues. + + Args: + file_path: Path to the file to analyze. + tool_context: The tool context from ADK. + + Returns: + Dict containing analysis results. + """ + try: + if not os.path.exists(file_path): + return {"error": f"File {file_path} does not exist", "status": "Failed"} + + with open(file_path, "r", encoding="utf-8") as file: + code = file.read() + + # Store the code in the state for the agent to access + tool_context.state["analyzed_code"] = code + tool_context.state["analyzed_file"] = file_path + + # Detect language + language = detect_language(file_path) + + # Initialize result object + result = CodeAnalysisResult(file_path=file_path, language=language, lines_of_code=len(code.split("\n")), status="Analysis complete") + + # Analyze based on language + if language == "python": + analysis = analyze_python_code(file_path, code) + result.issues = analysis["issues"] + result.metrics = analysis["metrics"] + elif language in ["javascript", "typescript"]: + analysis = analyze_javascript_code(file_path, code) + result.issues = analysis["issues"] + result.metrics = analysis["metrics"] + else: + result.issues.append(CodeIssue(severity=AnalysisSeverity.INFO, message=f"Analysis for {language} is not yet supported", source="analyzer")) + + # Store analysis issues in state for other tools to use + tool_context.state["analysis_issues"] = [issue.dict() for issue in result.issues] + + # Generate summary statistics + issue_counts = { + "critical": sum(1 for i in result.issues if i.severity == AnalysisSeverity.CRITICAL), + "error": sum(1 for i in result.issues if i.severity == AnalysisSeverity.ERROR), + "warning": sum(1 for i in result.issues if i.severity == AnalysisSeverity.WARNING), + "info": sum(1 for i in result.issues if i.severity == AnalysisSeverity.INFO), + } + result.metrics["issue_summary"] = issue_counts + + return result.dict() + except Exception as e: + return {"error": f"Error analyzing file: {e!s}", "status": "Failed"} + + +# Define the tool using FunctionTool +analyze_code_tool = FunctionTool(func=_analyze_code) + + +def get_issues_by_severity(tool_context: ToolContext, severity: Optional[str] = None) -> Dict[str, Any]: + """ + Retrieves code analysis issues filtered by severity. + + Args: + tool_context: The tool context from ADK. + severity: Optional severity level to filter by (critical, error, warning, info) + + Returns: + Dict containing filtered issues list. + """ + # Get all issues + issues = tool_context.state.get("analysis_issues", []) + + # If no severity specified, return all issues + if not severity: + return {"issues": issues} + + # Filter by severity + filtered_issues = [issue for issue in issues if issue.get("severity", "").lower() == severity.lower()] + + return {"issues": filtered_issues, "count": len(filtered_issues), "total_issues": len(issues), "severity": severity} + + +# Define additional tools to work with analysis results +get_analysis_issues_by_severity_tool = FunctionTool(func=get_issues_by_severity) + + +def suggest_fixes(tool_context: ToolContext) -> Dict[str, Any]: + """ + Analyzes issues and suggests fixes based on common patterns. + + Args: + tool_context: The tool context from ADK. + + Returns: + Dict containing suggested fixes for detected issues. + """ + issues = tool_context.state.get("analysis_issues", []) + + suggested_fixes = [] + + # Common patterns and their fixes + for issue in issues: + code = issue.get("code", "") + message = issue.get("message", "") + suggestion = None + + # Unused import suggestions + if "unused import" in message.lower(): + suggestion = f"Remove the unused import at line {issue.get('line')}" + + # Undefined variable suggestions + elif "undefined variable" in message.lower(): + var_name = message.split("'")[1] if "'" in message else "" + suggestion = f"Define variable '{var_name}' before use or check for typo" + + # Line too long + elif code == "E501" or "line too long" in message.lower(): + suggestion = f"Break the long line at {issue.get('line')} into multiple lines" + + # Complexity suggestions + elif "complexity" in message.lower(): + suggestion = f"Refactor the complex function at line {issue.get('line')} into smaller functions" + + if suggestion: + suggested_fixes.append({"issue": issue, "suggestion": suggestion}) + + return {"suggested_fixes": suggested_fixes, "count": len(suggested_fixes), "analyzed_file": tool_context.state.get("analyzed_file")} + + +# Define a tool for suggesting fixes +suggest_code_fixes_tool = FunctionTool(func=suggest_fixes) diff --git a/contributing/samples/software_engineer_litellm_llama/tools/code_search.py b/contributing/samples/software_engineer_litellm_llama/tools/code_search.py new file mode 100644 index 0000000000..36b1db1b71 --- /dev/null +++ b/contributing/samples/software_engineer_litellm_llama/tools/code_search.py @@ -0,0 +1,78 @@ +"""Code search functionality for software engineer agents using ripgrep.""" + +import json +import subprocess +from typing import Any, Dict, List, Optional + +from google.adk.tools import FunctionTool + + +def ripgrep_code_search(query: str, target_directories: Optional[List[str]] = None, explanation: Optional[str] = None) -> Dict[str, Any]: + """ + Perform a code search using ripgrep (rg) and return the results. + + Args: + query: The search query to find relevant code + target_directories: Optional list of directories to search in (glob patterns supported) + explanation: Optional explanation of why this search is being performed + + Returns: + Dictionary containing search results with snippets and file information + """ + try: + # Default to search in current directory if none specified + search_paths = target_directories or ["."] + + results = [] + for path in search_paths: + # Build the ripgrep command + # Using --json for structured output + # --context for showing surrounding lines + cmd = [ + "rg", + "--json", + "--context", + "2", # Show 2 lines before and after matches + "--max-columns", + "1000", # Reasonable line length limit + query, + ] + + # Add path to search + cmd.append(path) + + # Execute the search + process = subprocess.run( + cmd, + capture_output=True, + text=True, + check=False, # Don't raise exception if nothing found + ) + + # Process the output - each line is a JSON object + for line in process.stdout.strip().split("\n"): + if not line: + continue + + try: + data = json.loads(line) + + # Only process match data + if data.get("type") == "match": + file_path = data.get("data", {}).get("path", {}).get("text", "") + line_number = data.get("data", {}).get("line_number", 0) + match_content = data.get("data", {}).get("lines", {}).get("text", "").strip() + + results.append({"file": file_path, "line": line_number, "content": match_content}) + except json.JSONDecodeError: + # Skip lines that aren't valid JSON + continue + + return {"snippets": results, "status": "success", "query": query, "explanation": explanation or "Code search results"} + + except Exception as e: + return {"snippets": [], "status": "error", "error_message": str(e), "query": query} + + +# Create FunctionTool wrapper for ripgrep code search +codebase_search_tool = FunctionTool(func=ripgrep_code_search) diff --git a/contributing/samples/software_engineer_litellm_llama/tools/filesystem.py b/contributing/samples/software_engineer_litellm_llama/tools/filesystem.py new file mode 100644 index 0000000000..5ef75ab397 --- /dev/null +++ b/contributing/samples/software_engineer_litellm_llama/tools/filesystem.py @@ -0,0 +1,197 @@ +# code_agent/agent/software_engineer/software_engineer/tools/filesystem_tools.py +import logging +import os +from typing import Any, Dict + +from google.adk.tools import FunctionTool, ToolContext + +logger = logging.getLogger(__name__) + +# Consider adding a WORKSPACE_ROOT validation here for security +# WORKSPACE_ROOT = os.path.abspath(".") # Example: Use current working directory + + +def read_file_content(filepath: str) -> Dict[str, Any]: + """ + Reads the content of a file from the local filesystem. + + Args: + filepath: The relative or absolute path to the file. + Relative paths are resolved from the agent's current working directory. + (Security Note: Path validation should be implemented to restrict access). + + Returns: + A dictionary with: + - {'status': 'success', 'content': 'file_content_string'} on success. + - {'status': 'error', 'error_type': str, 'message': str} on failure. + Possible error_types: 'FileNotFound', 'PermissionDenied', 'IOError', 'SecurityViolation' (if implemented). + """ + logger.info(f"Attempting to read file: {filepath}") + # Add path validation/sandboxing here before opening + # Example: + # abs_path = os.path.abspath(filepath) + # if not abs_path.startswith(WORKSPACE_ROOT): + # message = f"Access denied: Path '{filepath}' is outside the allowed workspace." + # logger.error(message) + # return {"status": "error", "error_type": "SecurityViolation", "message": message} + try: + with open(filepath, "r", encoding="utf-8") as f: + content = f.read() + logger.info(f"Successfully read file: {filepath}") + return {"status": "success", "content": content} + except FileNotFoundError: + message = f"File not found at path '{filepath}'." + logger.error(message) + return {"status": "error", "error_type": "FileNotFound", "message": message} + except PermissionError: + message = f"Permission denied when trying to read file '{filepath}'." + logger.error(message) + return {"status": "error", "error_type": "PermissionDenied", "message": message} + except Exception as e: + message = f"An unexpected error occurred while reading file '{filepath}': {e}" + logger.error(message, exc_info=True) + return {"status": "error", "error_type": "IOError", "message": message} + + +def list_directory_contents(directory_path: str) -> Dict[str, Any]: + """ + Lists the contents (files and directories) of a directory on the local filesystem. + + Args: + directory_path: The relative or absolute path to the directory. + Relative paths are resolved from the agent's current working directory. + (Security Note: Path validation should be implemented to restrict access). + + Returns: + A dictionary with: + - {'status': 'success', 'contents': ['item1', 'item2', ...]} on success. + - {'status': 'error', 'error_type': str, 'message': str} on failure. + Possible error_types: 'NotADirectory', 'FileNotFound', 'PermissionDenied', 'IOError', 'SecurityViolation' (if implemented). + """ + logger.info(f"Attempting to list directory: {directory_path}") + # Add path validation/sandboxing here + # Example: + # abs_path = os.path.abspath(directory_path) + # if not abs_path.startswith(WORKSPACE_ROOT): + # message = f"Access denied: Path '{directory_path}' is outside the allowed workspace." + # logger.error(message) + # return {"status": "error", "error_type": "SecurityViolation", "message": message} + try: + if not os.path.isdir(directory_path): + message = f"The specified path '{directory_path}' is not a valid directory." + logger.warning(message) + return {"status": "error", "error_type": "NotADirectory", "message": message} + contents = os.listdir(directory_path) + logger.info(f"Successfully listed directory: {directory_path}") + return {"status": "success", "contents": contents} + except FileNotFoundError: + message = f"Directory not found at path '{directory_path}'." + logger.error(message) + return {"status": "error", "error_type": "FileNotFound", "message": message} + except PermissionError: + message = f"Permission denied when trying to list directory '{directory_path}'." + logger.error(message) + return {"status": "error", "error_type": "PermissionDenied", "message": message} + except Exception as e: + message = f"An unexpected error occurred while listing directory '{directory_path}': {e}" + logger.error(message, exc_info=True) + return {"status": "error", "error_type": "IOError", "message": message} + + +def edit_file_content(filepath: str, content: str, tool_context: ToolContext) -> Dict[str, Any]: + """ + Writes content to a file or proposes the write, requiring user approval based on session state. + Creates the file if it does not exist (including parent directories). + Overwrites the file if it already exists (only if approval is not required or already granted). + + Checks the 'require_edit_approval' flag in session state (defaults to True). + If True, returns a 'pending_approval' status without writing. + If False, writes the file and returns 'success' or 'error'. + + Args: + filepath: The relative or absolute path to the file. + Relative paths are resolved from the agent's current working directory. + (Security Note: Path validation should be implemented to restrict access). + content: The new content to write to the file. + + Returns: + A dictionary with: + - {'status': 'pending_approval', 'proposed_filepath': str, 'proposed_content': str, 'message': str} if approval is required. + - {'status': 'success', 'message': 'Success message'} on successful write (when approval not required). + - {'status': 'error', 'error_type': str, 'message': str} on failure during write or validation. + Possible error_types: 'PermissionDenied', 'IOError', 'SecurityViolation' (if implemented). + """ + logger.info(f"Checking approval requirement for writing to file: {filepath}") + + # Add path validation/sandboxing here FIRST + # Example: + # abs_path = os.path.abspath(filepath) + # if not abs_path.startswith(WORKSPACE_ROOT): + # message = f"Access denied: Path '{filepath}' is outside the allowed workspace." + # logger.error(message) + # return {"status": "error", "error_type": "SecurityViolation", "message": message} + + needs_approval = tool_context.state.get("require_edit_approval", True) + + if needs_approval: + logger.info(f"Approval required for file edit: {filepath}. Returning pending status.") + return { + "status": "pending_approval", + "proposed_filepath": filepath, + "proposed_content": content, + "message": f"Approval required to write to '{filepath}'. User confirmation needed.", + } + + # Proceed with write only if approval is not required + logger.info(f"Approval not required. Proceeding with write to file: {filepath}") + try: + # Ensure the directory exists + dir_path = os.path.dirname(filepath) + if dir_path: # Ensure dir_path is not empty (happens for root-level files) + os.makedirs(dir_path, exist_ok=True) # Creates parent dirs if needed + + # Consider atomic write here: write to temp file, then os.replace() + with open(filepath, "w", encoding="utf-8") as f: + f.write(content) + message = f"Successfully wrote content to '{filepath}'." + logger.info(message) + return {"status": "success", "message": message} + except PermissionError: + message = f"Permission denied when trying to write to file '{filepath}'." + logger.error(message) + return {"status": "error", "error_type": "PermissionDenied", "message": message} + except Exception as e: + message = f"An unexpected error occurred while writing to file '{filepath}': {e}" + logger.error(message, exc_info=True) + return {"status": "error", "error_type": "IOError", "message": message} + + +def configure_edit_approval(require_approval: bool, tool_context: ToolContext) -> Dict[str, Any]: + """ + Configures whether file edits require user approval for the current session. + Sets the 'require_edit_approval' flag in the session state. + + Note: This only affects file editing. For shell command approvals, use the + 'configure_shell_approval' function instead. + + Args: + require_approval: Set to True to require approval (default), False to allow direct edits. + + Returns: + A dictionary confirming the setting change: + - {'status': 'success', 'message': 'Confirmation message'} + """ + logger.info(f"Setting 'require_edit_approval' state to: {require_approval}") + tool_context.state["require_edit_approval"] = require_approval + message = f"File edit approval requirement set to: {require_approval} for this session." + logger.info(message) + return {"status": "success", "message": message} + + +# Wrap functions with FunctionTool +# Note: The return type for the tool schema remains the base function's return type hint (Dict[str, Any]) +read_file_tool = FunctionTool(read_file_content) +list_dir_tool = FunctionTool(list_directory_contents) +edit_file_tool = FunctionTool(edit_file_content) +configure_file_edit_approval_tool = FunctionTool(configure_edit_approval) +configure_approval_tool = configure_file_edit_approval_tool # Keep original name for backward compatibility diff --git a/contributing/samples/software_engineer_litellm_llama/tools/memory_tools.py b/contributing/samples/software_engineer_litellm_llama/tools/memory_tools.py new file mode 100644 index 0000000000..5193dcc198 --- /dev/null +++ b/contributing/samples/software_engineer_litellm_llama/tools/memory_tools.py @@ -0,0 +1,113 @@ +"""Tools for interacting with the persistent memory service.""" + +import logging +from typing import Any, Dict, List + +from google.adk.tools import FunctionTool +from google.adk.tools.tool_context import ToolContext + +logger = logging.getLogger(__name__) + + +def add_memory_fact_tool(tool_context: ToolContext, entity_name: str, fact_content: str) -> str: + """Adds a discrete fact to the memory service for the current session.""" + # Access services via _invocation_context + invocation_context = getattr(tool_context, "_invocation_context", None) + if not invocation_context: + logger.error("Invocation context not found in tool context for add_memory_fact.") + return "Error: Invocation context unavailable." + + memory_service = getattr(invocation_context, "memory_service", None) + # Session ID might be available directly on session object within invocation_context? + # Let's check for session first, then session_id directly + session = getattr(invocation_context, "session", None) + session_id = getattr(session, "id", None) if session else None + if not session_id: + # Fallback: check if session_id is directly on invocation_context (less likely) + session_id = getattr(invocation_context, "session_id", None) + + # --- Remove Debug --- # + # logger.warning(f"Inspecting tool_context in add_memory_fact_tool:") + # try: + # context_vars = vars(tool_context) + # logger.warning(f"vars(tool_context): {context_vars}") + # except TypeError: + # logger.warning("vars() failed, likely no __dict__. Trying dir():") + # context_dir = dir(tool_context) + # logger.warning(f"dir(tool_context): {context_dir}") + # --- End Debug --- + + # Old access method: + # memory_service = getattr(tool_context, "memory_service", None) + # session_id = getattr(tool_context, "session_id", None) + + if not memory_service: + logger.error("Memory service not available in invocation context for add_memory_fact.") + return "Error: Memory service is not available." + if not session_id: + logger.error("Session ID not available in invocation context for add_memory_fact.") + return "Error: Session ID is not available." + + # Check if add_observations method exists (duck typing) + if not hasattr(memory_service, "add_observations") or not callable(memory_service.add_observations): + logger.error("Memory service does not support 'add_observations'.") + return "Error: Memory service does not support adding observations." + + try: + # Structure the observation as a dictionary + observation = {"entity": entity_name, "content": fact_content} + logger.info(f"Adding memory fact for session {session_id}: {observation}") + # Call the underlying service method + memory_service.add_observations(session_id=session_id, observations=[observation]) + return f"Okay, I have remembered that {entity_name} is {fact_content}." + + except Exception as e: + logger.exception(f"Error calling memory_service.add_observations: {e}") + return f"Error: Failed to add fact to memory - {e}" + + +def search_memory_facts_tool(tool_context: ToolContext, query: str) -> List[Dict[str, Any]]: + """Searches discrete facts in the memory service for the current session.""" + # Access services via _invocation_context + invocation_context = getattr(tool_context, "_invocation_context", None) + if not invocation_context: + logger.error("Invocation context not found in tool context for search_memory_facts.") + return [{"error": "Invocation context unavailable."}] + + memory_service = getattr(invocation_context, "memory_service", None) + session = getattr(invocation_context, "session", None) + session_id = getattr(session, "id", None) if session else None + if not session_id: + session_id = getattr(invocation_context, "session_id", None) + + # Old access method: + # memory_service = getattr(tool_context, "memory_service", None) + # session_id = getattr(tool_context, "session_id", None) + + if not memory_service: + logger.error("Memory service not available in invocation context for search_memory_facts.") + return [{"error": "Memory service is not available."}] + if not session_id: + logger.error("Session ID not available in invocation context for search_memory_facts.") + return [{"error": "Session ID is not available."}] + + # Check if search_nodes method exists (duck typing) + if not hasattr(memory_service, "search_nodes") or not callable(memory_service.search_nodes): + logger.error("Memory service does not support 'search_nodes'.") + return [{"error": "Memory service does not support searching facts (nodes)."}] + + try: + logger.info(f"Searching memory facts for session {session_id} with query: '{query}'") + results = memory_service.search_nodes(session_id=session_id, query=query) + # Return the list of dictionaries directly + # The agent's prompt will need to guide it on interpreting this list + return results + + except Exception as e: + logger.exception(f"Error calling memory_service.search_nodes: {e}") + return [{"error": f"Failed to search facts in memory - {e}"}] + + +# Wrap functions with FunctionTool +add_memory_fact = FunctionTool(add_memory_fact_tool) +search_memory_facts = FunctionTool(search_memory_facts_tool) diff --git a/contributing/samples/software_engineer_litellm_llama/tools/persistent_memory_tool.py b/contributing/samples/software_engineer_litellm_llama/tools/persistent_memory_tool.py new file mode 100644 index 0000000000..5a8d0490c3 --- /dev/null +++ b/contributing/samples/software_engineer_litellm_llama/tools/persistent_memory_tool.py @@ -0,0 +1,155 @@ +"""Placeholder tools for manually saving/loading session memory to a file.""" + +import logging +from typing import Any, Dict + +from google.adk.tools import FunctionTool, ToolContext + +logger = logging.getLogger(__name__) + +# Default path for the memory file, could be configurable +DEFAULT_MEMORY_FILE = "./.manual_agent_memory.json" + +# === Tool Implementation Functions (Commented Out) === + + +def _save_current_session_to_file_impl(tool_context: ToolContext, filepath: str = DEFAULT_MEMORY_FILE) -> Dict[str, str]: + """ + (Placeholder) Saves the *current* session's state to a specified JSON file. + NOTE: This is a placeholder and not fully implemented. + + Args: + tool_context: The ADK tool context containing session information. + filepath: The path to the JSON file where the session should be saved. + + Returns: + A dictionary indicating the status of the operation. + """ + # TODO: Implement this tool if manual file-based persistence is needed + # for the standard 'adk run' environment. + # Implications: + # - Requires agent to be explicitly prompted to call this tool. + # - Overwrites the file with only the *current* session, or needs logic + # to merge with existing sessions in the file. + # - Doesn't integrate with the ADK's built-in MemoryService. + # - Needs robust error handling (file I/O, JSON serialization). + + logger.warning("Tool 'save_current_session_to_file' is a placeholder and not implemented.") + # --- Begin Commented Implementation Example --- + # if not hasattr(tool_context, 'session') or not tool_context.session: + # msg = "No active session found in tool_context." + # logger.error(msg) + # return {"status": "error", "message": msg} + # + # session: Session = tool_context.session + # logger.info(f"Attempting to save session {session.session_id} to {filepath}...") + # + # # Logic to load existing data, add/update the current session, and save back + # existing_data = {} + # if os.path.exists(filepath): + # try: + # with open(filepath, 'r', encoding='utf-8') as f: + # existing_data = json.load(f) + # logger.debug(f"Loaded {len(existing_data)} sessions from {filepath}") + # except (IOError, json.JSONDecodeError) as e: + # logger.error(f"Error reading existing memory file {filepath}: {e}. Overwriting may occur.") + # + # session_key = f"{session.app_name}_{session.user_id}_{session.session_id}" + # existing_data[session_key] = session.model_dump(mode='json') + # + # try: + # os.makedirs(os.path.dirname(filepath), exist_ok=True) + # with open(filepath, 'w', encoding='utf-8') as f: + # json.dump(existing_data, f, indent=4) + # logger.info(f"Successfully saved session {session.session_id} to {filepath}.") + # return {"status": "success", "message": f"Session saved to {filepath}"} + # except (IOError, TypeError) as e: + # msg = f"Error writing memory file {filepath}: {e}" + # logger.error(msg) + # return {"status": "error", "message": msg} + # --- End Commented Implementation Example --- + return {"status": "skipped", "message": "Tool is not implemented."} + + +def _load_memory_from_file_impl(query: str, filepath: str = DEFAULT_MEMORY_FILE) -> Dict[str, Any]: + """ + (Placeholder) Loads memory from a JSON file and performs a simple query. + NOTE: This is a placeholder and not fully implemented. + + Args: + query: The natural language query to search for in stored messages. + filepath: The path to the JSON file containing stored sessions. + + Returns: + A dictionary containing the search results or an error message. + """ + # TODO: Implement this tool if manual file-based persistence is needed + # for the standard 'adk run' environment. + # Implications: + # - Requires agent to be explicitly prompted to call this tool instead of load_memory. + # - Requires careful design of the query mechanism (e.g., simple substring search). + # - Doesn't integrate with the ADK's built-in MemoryService. + # - Needs robust error handling (file I/O, JSON deserialization, search logic). + + logger.warning("Tool 'load_memory_from_file' is a placeholder and not implemented.") + # --- Begin Commented Implementation Example --- + # if not os.path.exists(filepath): + # msg = f"Memory file not found: {filepath}" + # logger.error(msg) + # return {"status": "error", "message": msg, "results": []} + # + # try: + # with open(filepath, 'r', encoding='utf-8') as f: + # stored_sessions_data: Dict[str, Dict[str, Any]] = json.load(f) + # logger.info(f"Loaded {len(stored_sessions_data)} sessions from {filepath} for query: '{query}'") + # except (IOError, json.JSONDecodeError) as e: + # msg = f"Error reading memory file {filepath}: {e}" + # logger.error(msg) + # return {"status": "error", "message": msg, "results": []} + # + # results: List[Dict[str, Any]] = [] + # query_lower = query.lower() + # + # for session_key, session_data in stored_sessions_data.items(): + # try: + # # Minimal validation - check for history + # history = session_data.get('history', []) + # if not history: + # continue + # + # session_matched = False + # for message in history: + # if isinstance(message, dict) and 'parts' in message: + # message_text = "".join( + # [part.get('text', '') for part in message['parts'] if isinstance(part, dict)] + # ).lower() + # if query_lower in message_text: + # session_matched = True + # break + # + # if session_matched: + # logger.debug(f"Found match in session {session_key}") + # results.append({"session_key": session_key, "session_data": session_data}) + # # Limit results? Maybe return only the first few matches or most relevant? + # + # except Exception as e: + # logger.warning(f"Error processing session {session_key}: {e}. Skipping.") + # + # logger.info(f"Found {len(results)} relevant session(s) for query: '{query}'") + # return {"status": "success", "results": results} + # --- End Commented Implementation Example --- + return {"status": "skipped", "message": "Tool is not implemented.", "results": []} + + +# === Tool Definitions === + +# Wrap the placeholder functions with FunctionTool +save_current_session_to_file_tool = FunctionTool( + func=_save_current_session_to_file_impl, + # Name and description are inferred from the function docstring +) + +load_memory_from_file_tool = FunctionTool( + func=_load_memory_from_file_impl, + # Name and description are inferred from the function docstring +) diff --git a/contributing/samples/software_engineer_litellm_llama/tools/project_context.py b/contributing/samples/software_engineer_litellm_llama/tools/project_context.py new file mode 100644 index 0000000000..2d41fbf8fd --- /dev/null +++ b/contributing/samples/software_engineer_litellm_llama/tools/project_context.py @@ -0,0 +1,54 @@ +"""Project context tool for the software engineer agent.""" + +import json +import os + +from google.adk.agents.callback_context import CallbackContext + +# Define constants +PROJECT_CONTEXT_KEY = "project_context" +USER_PROFILE_KEY = "user_profile" +DEFAULT_CONTEXT_PATH = os.getenv("SOFTWARE_ENGINEER_CONTEXT", "eval/project_context_empty.json") + + +def load_project_context(callback_context: CallbackContext): + """ + Load the project context and user profile from a JSON file. + + Args: + callback_context: The callback context from ADK. + """ + # Initialize empty context + project_context = {} + user_profile = {} + + try: + if os.path.exists(DEFAULT_CONTEXT_PATH): + with open(DEFAULT_CONTEXT_PATH, "r") as file: + data = json.load(file) + project_context = data.get("project_context", {}) + user_profile = data.get("user_profile", {}) + print(f"\nLoaded project context: {project_context}\n") + print(f"\nLoaded user profile: {user_profile}\n") + except Exception as e: + print(f"Error loading project context: {e}") + + # Set the context in the state + callback_context.state[PROJECT_CONTEXT_KEY] = json.dumps(project_context, indent=2) + callback_context.state[USER_PROFILE_KEY] = json.dumps(user_profile, indent=2) + + +def memorize_context(key: str, value: str, context: dict): + """ + Store information in the project context. + + Args: + key: The key to store the value under. + value: The value to store. + context: The context dictionary to update. + + Returns: + A status message. + """ + context[key] = value + return {"status": f'Stored "{key}": "{value}" in project context'} diff --git a/contributing/samples/software_engineer_litellm_llama/tools/search.py b/contributing/samples/software_engineer_litellm_llama/tools/search.py new file mode 100644 index 0000000000..6b3141d149 --- /dev/null +++ b/contributing/samples/software_engineer_litellm_llama/tools/search.py @@ -0,0 +1,34 @@ +# ruff: noqa +# 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. + +"""Wrapper to Google Search Grounding with custom prompt.""" + +from google.adk.agents import Agent +from google.adk.tools.agent_tool import AgentTool +from google.adk.tools.google_search_tool import google_search + +_search_agent = Agent( + model="gemini-2.0-flash", + name="google_search_grounding", + description="An agent providing Google-search grounding capability", + instruction=""" + Answer the user's question directly using google_search grounding tool; Provide a brief but concise response. + Rather than a detail response, provide the immediate actionable item for a software engineer, in a single sentence. + Do not ask the user to check or look up information for themselves, that's your role; do your best to be informative. + """, + tools=[google_search], +) + +google_search_grounding = AgentTool(agent=_search_agent) diff --git a/contributing/samples/software_engineer_litellm_llama/tools/shell_command.py b/contributing/samples/software_engineer_litellm_llama/tools/shell_command.py new file mode 100644 index 0000000000..1be279feeb --- /dev/null +++ b/contributing/samples/software_engineer_litellm_llama/tools/shell_command.py @@ -0,0 +1,516 @@ +import ast +import json +import logging +import os +import shlex +import shutil # <-- Added import +import subprocess +from typing import Literal, Optional + +# Import ToolContext for state management +from google.adk.tools import ( + FunctionTool, # Ensure FunctionTool is imported if not already + ToolContext, +) +from pydantic import BaseModel, Field + +logger = logging.getLogger(__name__) + + +def parse_args(args): + """Utility function to parse arguments for tool functions. + + Handles both dictionary and string arguments for compatibility with + different LLM models (Gemini uses dict, LLaMA uses str). + + Args: + args: The arguments to parse, either as a dictionary or string. + + Returns: + dict: The parsed arguments as a dictionary. + + Raises: + ValueError: If the arguments cannot be parsed. + """ + if isinstance(args, dict): + return args + elif isinstance(args, str): + try: + # Try parsing as JSON first + try: + return json.loads(args) + except json.JSONDecodeError: + # If not valid JSON, try parsing as a Python literal + return ast.literal_eval(args) + except (ValueError, SyntaxError, AttributeError) as e: + logger.error(f"Failed to parse args string: {e}") + raise ValueError(f"Failed to parse arguments: {e}") + else: + logger.error(f"Unsupported args type: {type(args)}") + raise ValueError(f"Unsupported args type: {type(args)}") + + +# --- Configuration Tool --- # + + +class ConfigureShellApprovalInput(BaseModel): + """Input model for configuring shell command approval.""" + + require_approval: bool = Field(..., description="Set to true to require approval for shell commands, false to disable.") + + +class ConfigureShellApprovalOutput(BaseModel): + """Output model for configuring shell command approval.""" + + status: str + + +def configure_shell_approval(args: dict, tool_context: ToolContext) -> ConfigureShellApprovalOutput: + """Configures whether running shell commands requires user approval for the current session. + This is separate from file edit approvals which are managed by configure_edit_approval. + + Args: + args (dict): A dictionary containing: + require_approval (bool): Set to true to require approval for shell commands, false to disable. + Also handles string representation of arguments for LLaMA models. + tool_context (ToolContext): The context for accessing session state. + """ + try: + args_dict = parse_args(args) + require_approval = args_dict.get("require_approval") + except ValueError as e: + return ConfigureShellApprovalOutput(status=str(e)) + + # Convert string representations of booleans to actual booleans + if isinstance(require_approval, str): + if require_approval.lower() == "true": + require_approval = True + elif require_approval.lower() == "false": + require_approval = False + + # Add validation for the boolean argument + if require_approval is None or not isinstance(require_approval, bool): + message = "Error: 'require_approval' argument is missing or not a boolean (true/false)." + logger.error(message) + return ConfigureShellApprovalOutput(status=message) + + tool_context.state["require_shell_approval"] = require_approval + status = "enabled" if require_approval else "disabled" + logger.info(f"Shell command approval requirement set to: {status}") + return ConfigureShellApprovalOutput( + status=f"Shell command approval requirement is now {status}. Shell commands {'' if require_approval else 'no longer '}require explicit approval." + ) + + +# --- Whitelist Configuration Tool --- # + + +class ConfigureShellWhitelistInput(BaseModel): + """Input model for configuring the shell command whitelist.""" + + action: Literal["add", "remove", "list", "clear"] = Field(..., description="Action to perform: add, remove, list, or clear.") + command: Optional[str] = Field(None, description="The command to add or remove (required for 'add' and 'remove' actions).") + + +class ConfigureShellWhitelistOutput(BaseModel): + """Output model for configuring the shell command whitelist.""" + + status: str + whitelist: Optional[list[str]] = Field(None, description="The current whitelist (only for 'list' action).") + + +def configure_shell_whitelist(args: dict, tool_context: ToolContext) -> ConfigureShellWhitelistOutput: + """Manages the whitelist of shell commands that bypass approval. + + Note: To completely disable shell approval checks, use the `configure_shell_approval` + function with `require_approval` set to `false` instead. + + Args: + args (dict): A dictionary containing: + action (Literal["add", "remove", "list", "clear"]): The action. + command (Optional[str]): The command for add/remove. + Also handles string representation of arguments for LLaMA models. + tool_context (ToolContext): The context for accessing session state. + """ + try: + args_dict = parse_args(args) + action = args_dict.get("action") + command = args_dict.get("command") + except ValueError as e: + return ConfigureShellWhitelistOutput(status=str(e)) + + # Default safe commands (adjust as needed) + DEFAULT_SAFE_COMMANDS = [ + "ls", + "grep", + "find", + "cat", + "pwd", + "echo", + "git status", + "head", + "tail", + "wc", + "git diff", + "git log", + "which", + "ping", + "host", + "dig", + "nslookup", + "ss", + "uname", + "uptime", + "date", + "time", + "df", + "du", + "free", + "stat", + "ps", + "pgrep", + "ip addr", + "ip route", + "traceroute", + "git grep", + "git branch", + "git branch --show-current", # Specific safe variant + "git tag", + "git remote -v", + "git config --list", + "docker ps", + "docker images", + "kubectl get", + "kubectl describe", + "kubectl logs", + "kubectl cluster-info", + "kubectl config view", + "kubectl version", + "kubectl api-resources", + "kubectl api-versions", + "kubectl top", + ] + + # Initialize whitelist in state if it doesn't exist + if "shell_command_whitelist" not in tool_context.state: + # Initialize with default safe commands + tool_context.state["shell_command_whitelist"] = DEFAULT_SAFE_COMMANDS[:] + logger.info(f"Initialized shell command whitelist with defaults: {DEFAULT_SAFE_COMMANDS}") + + whitelist: list[str] = tool_context.state["shell_command_whitelist"] + + if action == "add": + if not command: + return ConfigureShellWhitelistOutput(status="Error: 'command' is required for 'add' action.") + if command not in whitelist: + whitelist.append(command) + tool_context.state["shell_command_whitelist"] = whitelist # Update state + logger.info(f"Added command '{command}' to shell whitelist.") + return ConfigureShellWhitelistOutput(status=f"Command '{command}' added to whitelist.") + else: + return ConfigureShellWhitelistOutput(status=f"Command '{command}' is already in the whitelist.") + elif action == "remove": + if not command: + return ConfigureShellWhitelistOutput(status="Error: 'command' is required for 'remove' action.") + if command in whitelist: + whitelist.remove(command) + tool_context.state["shell_command_whitelist"] = whitelist # Update state + logger.info(f"Removed command '{command}' from shell whitelist.") + return ConfigureShellWhitelistOutput(status=f"Command '{command}' removed from whitelist.") + else: + return ConfigureShellWhitelistOutput(status=f"Command '{command}' not found in whitelist.") + elif action == "list": + return ConfigureShellWhitelistOutput(status="Current whitelist retrieved.", whitelist=list(whitelist)) # Return a copy + elif action == "clear": + tool_context.state["shell_command_whitelist"] = [] + logger.info("Cleared shell command whitelist.") + return ConfigureShellWhitelistOutput(status="Shell command whitelist cleared.") + else: + return ConfigureShellWhitelistOutput(status=f"Error: Invalid action '{action}'. Valid actions are: add, remove, list, clear.") + + +# --- Check Command Existence Tool --- # + + +class CheckCommandExistsInput(BaseModel): + """Input model for checking command existence.""" + + command: str = Field(..., description="The command name (e.g., 'git', 'ls') to check for existence.") + + +class CheckCommandExistsOutput(BaseModel): + """Output model for checking command existence.""" + + exists: bool + command_checked: str + message: str + + +def check_command_exists(args: dict, tool_context: ToolContext) -> CheckCommandExistsOutput: + """Checks if a command exists in the system's PATH. Extracts the base command. + + Args: + args (dict): A dictionary containing: + command (str): The command name to check. + Also handles string representation of arguments for LLaMA models. + tool_context (ToolContext): The context for accessing session state. + """ + try: + args_dict = parse_args(args) + command_name = args_dict.get("command") + except ValueError as e: + return CheckCommandExistsOutput(exists=False, command_checked="", message=str(e)) + + base_command = None + message = "" + + if not command_name: + message = "Error: 'command' argument is missing." + logger.error(message) + return CheckCommandExistsOutput(exists=False, command_checked=command_name or "", message=message) + + try: + # Extract base command if it includes arguments (shutil.which needs the command name only) + parts = shlex.split(command_name) + if parts: + base_command = parts[0] + else: + message = f"Could not parse base command from input: '{command_name}'" + logger.warning(message) + return CheckCommandExistsOutput(exists=False, command_checked=command_name, message=message) + + except ValueError as e: + message = f"Error parsing command '{command_name}': {e}" + logger.error(message) + return CheckCommandExistsOutput(exists=False, command_checked=command_name, message=message) + + if not base_command: # Should not happen if parsing worked, but check anyway + message = "Error: Could not determine base command." + logger.error(message) + return CheckCommandExistsOutput(exists=False, command_checked=command_name, message=message) + + exists = shutil.which(base_command) is not None + status_msg = "exists" if exists else "does not exist" + message = f"Command '{base_command}' {status_msg} in system PATH." + logger.info(f"Checked existence for command '{base_command}': {exists}") + return CheckCommandExistsOutput(exists=exists, command_checked=base_command, message=message) + + +# --- Shell Command Safety Check Tool --- # + + +class CheckShellCommandSafetyInput(BaseModel): + """Input model for checking shell command safety.""" + + command: str = Field(..., description="The shell command to check.") + + +class CheckShellCommandSafetyOutput(BaseModel): + """Output model for checking shell command safety.""" + + status: Literal["whitelisted", "approval_disabled", "approval_required"] = Field(..., description="The safety status of the command.") + command: str = Field(..., description="The command that was checked.") + message: str = Field(..., description="Explanation of the status.") + + +def check_shell_command_safety(args: dict, tool_context: ToolContext) -> CheckShellCommandSafetyOutput: + """Checks if a shell command is safe to run without explicit user approval. + + Checks against the configured whitelist and the session's approval requirement. + Does NOT execute the command. + + Args: + args (dict): A dictionary containing: + command (str): The shell command to check. + Also handles string representation of arguments for LLaMA models. + tool_context (ToolContext): The context for accessing session state. + """ + try: + args_dict = parse_args(args) + command = args_dict.get("command") + except ValueError as e: + return CheckShellCommandSafetyOutput(status="approval_required", command=str(args), message=str(e)) + + if not command: + # Technically this shouldn't happen with Pydantic validation, but belt-and-suspenders + return CheckShellCommandSafetyOutput( + status="approval_required", + command="[no command provided]", + message="No command string was provided to check.", + ) + + # Check if shell approval is disabled globally + if "require_shell_approval" in tool_context.state and tool_context.state["require_shell_approval"] is False: + return CheckShellCommandSafetyOutput( + status="approval_disabled", + command=command, + message="Shell command approval is disabled for this session.", + ) + + # Check if the command is in the whitelist + whitelist = tool_context.state.get("shell_command_whitelist", []) + if command in whitelist: + return CheckShellCommandSafetyOutput( + status="whitelisted", + command=command, + message=f"Command '{command}' is in the whitelist.", + ) + + # Default: require approval for any command not explicitly whitelisted + return CheckShellCommandSafetyOutput( + status="approval_required", + command=command, + message=f"Command '{command}' requires explicit approval.", + ) + + +# --- Vetted Shell Command Execution Tool --- # + + +class ExecuteVettedShellCommandInput(BaseModel): + """Input model for the execute_vetted_shell_command tool.""" + + command: str = Field(..., description="The shell command to execute. Should have been vetted first.") + working_directory: Optional[str] = Field(None, description="Optional working directory to run the command in.") + timeout: int = Field(60, description="Timeout in seconds for the command execution.") + + +class ExecuteVettedShellCommandOutput(BaseModel): + """Output model for the execute_vetted_shell_command tool.""" + + stdout: str | None = Field(None, description="The standard output of the command.") + stderr: str | None = Field(None, description="The standard error of the command.") + return_code: int | None = Field(None, description="The return code of the command.") + command_executed: str | None = Field(None, description="The command that was executed.") + status: str = Field(description="Status: 'executed' or 'error'.") + message: str = Field(description="Additional information about the status.") + + +def execute_vetted_shell_command(args: dict, tool_context: ToolContext) -> ExecuteVettedShellCommandOutput: + """Executes a shell command that has already been verified as safe or approved. + + Args: + args (dict): A dictionary containing: + command (str): The shell command to execute. + working_directory (Optional[str]): Optional working directory. + timeout (Optional[int]): Timeout in seconds (default: 60). + Also handles string representation of arguments for LLaMA models. + tool_context (ToolContext): The context for the tool. + + Returns: + ExecuteVettedShellCommandOutput: Result of the command execution. + """ + try: + args_dict = parse_args(args) + command = args_dict.get("command") + working_directory = args_dict.get("working_directory") + timeout = args_dict.get("timeout", 60) + except ValueError as e: + return ExecuteVettedShellCommandOutput(status="error", message=str(e), command_executed=str(args)) + + if not command: + logger.error("No command provided for execution.") + return ExecuteVettedShellCommandOutput( + status="error", + message="No command provided for execution.", + command_executed=None, + stdout=None, + stderr=None, + return_code=None, + ) + + # Ensure timeout is an integer + try: + timeout = int(timeout) + except (ValueError, TypeError): + logger.warning(f"Invalid timeout value: {timeout}, using default 60 seconds.") + timeout = 60 + + # Use working_directory if provided and it exists + cwd = None + if working_directory: + if not os.path.isdir(working_directory): + logger.warning(f"Working directory '{working_directory}' does not exist, using current directory.") + else: + cwd = working_directory + + # Set environment variables to prevent interactive prompts + # And make sure we're not using ANSI colors in the output + env = os.environ.copy() + env.update( + { + "GIT_TERMINAL_PROMPT": "0", # Don't prompt for credentials + "TERM": "dumb", # Disable ANSI colors + "FORCE_COLOR": "0", # Disable colored output + "NO_COLOR": "1", # Disable colored output + "CI": "1", # Pretend we're in a CI environment + } + ) + + try: + logger.info(f"Executing command: {command}") + # Use shell=True for complex commands with pipes, redirects, etc. + # This is safe because we've already vetted the command + # But we should still be cautious about wildcard expansion and special chars + process = subprocess.run( + command, + capture_output=True, + text=True, + shell=True, + cwd=cwd, + timeout=timeout, + env=env, + ) + + stdout = process.stdout or "" + stderr = process.stderr or "" + return_code = process.returncode + + # Log the result + log_msg = f"Command '{command}' executed with return code {return_code}" + if return_code == 0: + logger.info(log_msg) + else: + logger.warning(f"{log_msg}. stderr: {stderr[:100]}...") + + # Return the result + return ExecuteVettedShellCommandOutput( + stdout=stdout, + stderr=stderr, + return_code=return_code, + command_executed=command, + status="executed", + message=f"Command executed with return code {return_code}.", + ) + except subprocess.TimeoutExpired: + logger.error(f"Command '{command}' timed out after {timeout} seconds.") + return ExecuteVettedShellCommandOutput( + stdout=None, + stderr=None, + return_code=None, + command_executed=command, + status="error", + message=f"Command timed out after {timeout} seconds.", + ) + except Exception as e: + logger.error(f"Error executing command '{command}': {e}") + return ExecuteVettedShellCommandOutput( + stdout=None, + stderr=str(e), + return_code=None, + command_executed=command, + status="error", + message=f"Command execution error: {e}", + ) + + +# --- Tool Registrations --- # <-- Added section (optional but good practice) + +# Wrap functions with FunctionTool +# Note: This assumes FunctionTool is imported or available in the scope + +configure_shell_approval_tool = FunctionTool(configure_shell_approval) +configure_shell_whitelist_tool = FunctionTool(configure_shell_whitelist) +check_command_exists_tool = FunctionTool(check_command_exists) # <-- Added tool +check_shell_command_safety_tool = FunctionTool(check_shell_command_safety) +execute_vetted_shell_command_tool = FunctionTool(execute_vetted_shell_command) diff --git a/contributing/samples/software_engineer_litellm_llama/tools/system_info.py b/contributing/samples/software_engineer_litellm_llama/tools/system_info.py new file mode 100644 index 0000000000..61528c01df --- /dev/null +++ b/contributing/samples/software_engineer_litellm_llama/tools/system_info.py @@ -0,0 +1,89 @@ +""" +Tools for getting system information. + +NOTE: This is needed for LiteLLM models in order to use the FunctionTool. +""" + +import logging +import platform +from typing import Any, Dict + +from google.adk.tools import FunctionTool, ToolContext +from pydantic import BaseModel, Field + +logger = logging.getLogger(__name__) + + +class OSInfoOutput(BaseModel): + """Output model for the get_os_info tool.""" + + system: str = Field(description="Operating system name (e.g., 'Linux', 'Darwin', 'Windows').") + release: str = Field(description="Operating system release (e.g., '5.15.0-78-generic').") + version: str = Field(description="Operating system version.") + machine: str = Field(description="Machine hardware name (e.g., 'x86_64').") + + +def get_os_info(args: dict, tool_context: ToolContext) -> Dict[str, Any]: + """Gets information about the operating system. + + Args: + args: Does not require any arguments. + tool_context: The tool context. + + Returns: + A dictionary with information about the operating system. + """ + os_info = { + "system": platform.system(), + "node": platform.node(), + "release": platform.release(), + "version": platform.version(), + "machine": platform.machine(), + "processor": platform.processor(), + "python_version": platform.python_version(), + } + return {"os_info": os_info} + + +def list_available_tools(args: dict, tool_context: ToolContext) -> Dict[str, Any]: + """Lists all available tools in the agent. + + Args: + args: Arguments (not used) + tool_context: The tool context + + Returns: + Dictionary containing the list of available tools and their descriptions + """ + # List all tools registered with the agent + tools = { + "file_system_tools": { + "read_file_tool": "Reads the content of a file", + "list_dir_tool": "Lists the contents of a directory", + "edit_file_tool": "Edits or creates a file", + "configure_edit_approval_tool": "Configures whether file edits require approval", + }, + "shell_command_tools": { + "check_command_exists_tool": "Checks if a command exists in the system", + "check_shell_command_safety_tool": "Checks if a shell command is safe to run", + "configure_shell_approval_tool": "Configures approval requirements for shell commands", + "configure_shell_whitelist_tool": "Manages the whitelist of shell commands", + "execute_vetted_shell_command_tool": "Executes a vetted shell command", + }, + "search_tools": {"google_search_grounding": "Searches the web for information", "codebase_search_tool": "Searches for code in the codebase"}, + "system_info_tools": { + "get_os_info_tool": "Gets information about the operating system", + "list_available_tools_tool": "Lists all available tools in the agent", + }, + } + + return {"tools": tools, "message": "Use these tools directly by calling them with appropriate arguments. No need to discover them first."} + + +# Wrap get_os_info with FunctionTool +get_os_info_tool = FunctionTool(get_os_info) +list_available_tools_tool = FunctionTool(list_available_tools) + +# Add aliases for different naming conventions that various models might use +list_tools_tool = list_available_tools_tool # Alias for shorter name +available_tools_tool = list_available_tools_tool # Another common name diff --git a/pyproject.toml b/pyproject.toml index e89c9656b9..73c00a4fab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,11 +25,12 @@ classifiers = [ # List of https://pypi.org/classifiers/ ] dependencies = [ # go/keep-sorted start - "PyYAML>=6.0.2, <7.0.0", # For APIHubToolset. - "absolufy-imports>=0.3.1, <1.0.0", # For Agent Engine deployment. - "anyio>=4.9.0, <5.0.0;python_version>='3.10'", # For MCP Session Manager - "authlib>=1.5.1, <2.0.0", # For RestAPI Tool - "click>=8.1.8, <9.0.0", # For CLI tools + "PyYAML>=6.0.2, <7.0.0", # For APIHubToolset. + "absolufy-imports>=0.3.1, <1.0.0", # For Agent Engine deployment. + "anyio>=4.9.0, <5.0.0;python_version>='3.10'", # For MCP Session Manager + "authlib>=1.5.1, <2.0.0", # For RestAPI Tool + #"click>=8.1.8, <9.0.0", # For CLI tools + "rich-click>=1.8.8", # For CLI tools "fastapi>=0.115.0, <1.0.0", # FastAPI framework "google-api-python-client>=2.157.0, <3.0.0", # Google API client discovery "google-cloud-bigtable>=2.32.0", # For Bigtable database diff --git a/src/google/adk/cli/cli.py b/src/google/adk/cli/cli.py index 70c58d04c3..feaba856f5 100644 --- a/src/google/adk/cli/cli.py +++ b/src/google/adk/cli/cli.py @@ -17,9 +17,11 @@ from datetime import datetime from typing import Optional -import click +import rich_click as click from google.genai import types from pydantic import BaseModel +from rich.console import Console +from rich.markdown import Markdown from ..agents.llm_agent import LlmAgent from ..artifacts.base_artifact_service import BaseArtifactService @@ -56,6 +58,8 @@ async def run_input_file( session_service=session_service, credential_service=credential_service, ) + # Initialize Rich Console + console = Console() with open(input_path, 'r', encoding='utf-8') as f: input_file = InputFile.model_validate_json(f.read()) input_file.state['_time'] = datetime.now() @@ -63,8 +67,9 @@ async def run_input_file( session = await session_service.create_session( app_name=app_name, user_id=user_id, state=input_file.state ) + console.print(f"[bold blue]Running from input file:[/bold blue] [blue]{input_path}[/blue]") for query in input_file.queries: - click.echo(f'[user]: {query}') + console.print(f'[blue][user][/blue]: {query}') content = types.Content(role='user', parts=[types.Part(text=query)]) async with Aclosing( runner.run_async( @@ -74,7 +79,7 @@ async def run_input_file( async for event in agen: if event.content and event.content.parts: if text := ''.join(part.text or '' for part in event.content.parts): - click.echo(f'[{event.author}]: {text}') + console.print(f'[green][{event.author}][/green]: {text}') return session @@ -92,8 +97,10 @@ async def run_interactively( session_service=session_service, credential_service=credential_service, ) + # Initialize Rich Console + console = Console() while True: - query = input('[user]: ') + query = console.input('😎 [blue]user[/blue] > ') if not query or not query.strip(): continue if query == 'exit': @@ -110,7 +117,8 @@ async def run_interactively( async for event in agen: if event.content and event.content.parts: if text := ''.join(part.text or '' for part in event.content.parts): - click.echo(f'[{event.author}]: {text}') + markdown_text = Markdown(text) + console.print(f'🤖 [green]{event.author}[/green] > ', markdown_text) await runner.close() @@ -138,6 +146,9 @@ async def run_cli( session_id: Optional[str], the session ID to save the session to on exit. """ + # Initialize Rich Console + console = Console() + artifact_service = InMemoryArtifactService() session_service = InMemorySessionService() credential_service = InMemoryCredentialService() @@ -165,15 +176,16 @@ async def run_cli( loaded_session = Session.model_validate_json(f.read()) if loaded_session: + console.print(f"[bold blue]Loading session from[/bold blue] [blue]{saved_session_file}[/blue]") for event in loaded_session.events: await session_service.append_event(session, event) content = event.content if not content or not content.parts or not content.parts[0].text: continue if event.author == 'user': - click.echo(f'[user]: {content.parts[0].text}') + console.print(f'[blue][user][/blue]: {content.parts[0].text}') else: - click.echo(f'[{event.author}]: {content.parts[0].text}') + console.print(f'[{event.author}]: {content.parts[0].text}') await run_interactively( root_agent, @@ -183,7 +195,7 @@ async def run_cli( credential_service, ) else: - click.echo(f'Running agent {root_agent.name}, type exit to exit.') + console.print(f'[yellow]Running agent [green]{root_agent.name}[/green], type exit to exit.[/yellow]') await run_interactively( root_agent, artifact_service, @@ -207,4 +219,4 @@ async def run_cli( with open(session_path, 'w', encoding='utf-8') as f: f.write(session.model_dump_json(indent=2, exclude_none=True)) - print('Session saved to', session_path) + console.print(f'Session saved to [bold]{session_path}[/bold]') diff --git a/src/google/adk/cli/cli_create.py b/src/google/adk/cli/cli_create.py index 9085586e18..5b3d4b2b9b 100644 --- a/src/google/adk/cli/cli_create.py +++ b/src/google/adk/cli/cli_create.py @@ -19,7 +19,7 @@ from typing import Optional from typing import Tuple -import click +import rich_click as click _INIT_PY_TEMPLATE = """\ from . import agent diff --git a/src/google/adk/cli/cli_deploy.py b/src/google/adk/cli/cli_deploy.py index 2632af9b23..103708a546 100644 --- a/src/google/adk/cli/cli_deploy.py +++ b/src/google/adk/cli/cli_deploy.py @@ -18,7 +18,7 @@ import subprocess from typing import Optional -import click +import rich_click as click from packaging.version import parse _DOCKERFILE_TEMPLATE = """ diff --git a/src/google/adk/cli/cli_tools_click.py b/src/google/adk/cli/cli_tools_click.py index 3000edf788..bbcb6ea1ae 100644 --- a/src/google/adk/cli/cli_tools_click.py +++ b/src/google/adk/cli/cli_tools_click.py @@ -24,8 +24,9 @@ import tempfile from typing import Optional -import click +# import click from click.core import ParameterSource +import rich_click as click from fastapi import FastAPI import uvicorn diff --git a/src/google/adk/cli/fast_api.py b/src/google/adk/cli/fast_api.py index 7d93b54360..c65784c442 100644 --- a/src/google/adk/cli/fast_api.py +++ b/src/google/adk/cli/fast_api.py @@ -23,7 +23,7 @@ from typing import Mapping from typing import Optional -import click +import rich_click as click from fastapi import FastAPI from fastapi import UploadFile from fastapi.responses import FileResponse