You've already forked adk-python
mirror of
https://github.com/encounter/adk-python.git
synced 2026-07-09 18:19:28 -07:00
Compare commits
96 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c6cf11cb74 | |||
| 2724819622 | |||
| 2f4f5611bd | |||
| 4df926388b | |||
| d4dc645478 | |||
| 7d5c6b9acf | |||
| aab2504ebd | |||
| 391628fcdc | |||
| 409df1378f | |||
| 2a901d12f4 | |||
| c850da3a07 | |||
| ed37e343f0 | |||
| ce3418a69d | |||
| fe1fc75c15 | |||
| dc4975dea9 | |||
| aeaec859bf | |||
| 4a842c5a13 | |||
| 0bdba30263 | |||
| 6a5eac0bdc | |||
| 0b73a6937b | |||
| ee39a89110 | |||
| d327538724 | |||
| a5b742b360 | |||
| af74eba695 | |||
| dbd818be0b | |||
| a2d9f13fa1 | |||
| 0df67599c0 | |||
| 8b3ed059c2 | |||
| cf3403231d | |||
| 9dce06f9b0 | |||
| 307896aece | |||
| 6dcbb5aca6 | |||
| 2a8fdd94e1 | |||
| 37a153ef94 | |||
| ce4638651f | |||
| e6e2767c39 | |||
| e50f05a9fc | |||
| 9dc00360e3 | |||
| 5a543c00df | |||
| 1e1d63f34c | |||
| f51380f9ea | |||
| 3734ceaa6c | |||
| 86097afe49 | |||
| a17f3b2e6d | |||
| 6ab1498aa0 | |||
| 2424d6a3b1 | |||
| 36c96ec5b3 | |||
| a985cc38ec | |||
| b650181384 | |||
| 78e74b5bf2 | |||
| d82c492140 | |||
| 05aa3fa38b | |||
| f9c09ef075 | |||
| 141318f775 | |||
| 85c95a8cbf | |||
| cb6d583cad | |||
| 2c7a342593 | |||
| 9fbed0b15a | |||
| bae21027d9 | |||
| 89344da813 | |||
| d22b8bf890 | |||
| dfb8638eae | |||
| 3c0b99a19e | |||
| d8548aabd2 | |||
| df05ed6b3b | |||
| 2158b3c915 | |||
| e2072af69f | |||
| fa84bcb575 | |||
| bb1ea74924 | |||
| 214986ebeb | |||
| 348e552ba6 | |||
| e212ff558e | |||
| e63180cb62 | |||
| 6da7274858 | |||
| b21d0a50d6 | |||
| 16b030b2b2 | |||
| 59670d240e | |||
| bddc70b5d0 | |||
| 85ed500871 | |||
| 998264a5b1 | |||
| 9a6b8507f0 | |||
| 64646e0002 | |||
| 81913c85f4 | |||
| 9e0b1fb62b | |||
| 731bb9078d | |||
| ae139bb461 | |||
| 9939e0b087 | |||
| cc24d616f8 | |||
| 0aede9f1a1 | |||
| 847df1638c | |||
| 55aa6f669b | |||
| 9b8a4aad6f | |||
| cac9fae829 | |||
| 24342e95f8 | |||
| cbe60c47aa | |||
| 2efaa57575 |
@@ -3,10 +3,16 @@ name: ADK Pull Request Triaging Agent
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened, edited]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: 'The Pull Request number to triage'
|
||||
required: true
|
||||
type: 'string'
|
||||
|
||||
jobs:
|
||||
agent-triage-pull-request:
|
||||
if: "!contains(github.event.pull_request.labels.*.name, 'bot triaged') && !contains(github.event.pull_request.labels.*.name, 'google-contributor')"
|
||||
if: github.event_name == 'workflow_dispatch' || !contains(github.event.pull_request.labels.*.name, 'google-contributor')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
@@ -33,7 +39,7 @@ jobs:
|
||||
GOOGLE_GENAI_USE_VERTEXAI: 0
|
||||
OWNER: 'google'
|
||||
REPO: 'adk-python'
|
||||
PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }}
|
||||
PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number || github.event.inputs.pr_number }}
|
||||
INTERACTIVE: ${{ vars.PR_TRIAGE_INTERACTIVE }}
|
||||
PYTHONPATH: contributing/samples
|
||||
run: python -m adk_pr_triaging_agent.main
|
||||
|
||||
@@ -3,8 +3,6 @@ name: ADK Issue Triaging Agent
|
||||
on:
|
||||
issues:
|
||||
types: [opened, reopened]
|
||||
schedule:
|
||||
- cron: '0 */6 * * *' # every 6h
|
||||
|
||||
jobs:
|
||||
agent-triage-issues:
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
* Update remote_a2a_agent to better handle streaming events and avoid duplicate responses ([8e5f361](https://github.com/google/adk-python/commit/8e5f36126498f751171bb2639c7f5a9e7dca2558))
|
||||
* Update the load_artifacts tool so that the model can reliably call it for follow up questions about the same artifact ([238472d](https://github.com/google/adk-python/commit/238472d083b5aa67551bde733fc47826ff062679))
|
||||
* Fix VertexAiSessionService base_url override to preserve initialized http_options ([8110e41](https://github.com/google/adk-python/commit/8110e41b36cceddb8b92ba17cffaacf701706b36), [c51ea0b](https://github.com/google/adk-python/commit/c51ea0b52e63de8e43d3dccb24f9d20987784aa5))
|
||||
* Handle `App` instances returned by `agent_loader.load_agent` ([847df16](https://github.com/google/adk-python/commit/847df1638cbf1686aa43e8e094121d4e23e40245))
|
||||
|
||||
### Improvements
|
||||
|
||||
|
||||
@@ -25,14 +25,44 @@
|
||||
Agent Development Kit (ADK) is a flexible and modular framework for developing and deploying AI agents. While optimized for Gemini and the Google ecosystem, ADK is model-agnostic, deployment-agnostic, and is built for compatibility with other frameworks. ADK was designed to make agent development feel more like software development, to make it easier for developers to create, deploy, and orchestrate agentic architectures that range from simple tasks to complex workflows.
|
||||
|
||||
|
||||
---
|
||||
## 🔥 ADK's very first community call on Oct 15
|
||||
|
||||
Join our ADK Community Call! Our first virtual community call is on Oct 15!
|
||||
Meet our team, and talk with us about our roadmap and how to contribute.
|
||||
|
||||
First Call Details:
|
||||
|
||||
Topic: ADK Roadmap
|
||||
|
||||
Date: October 15, 2025
|
||||
|
||||
Time: 9:30-10:30am PST
|
||||
|
||||
Meeting link:
|
||||
[Join the call](http://meet.google.com/gjm-gfim-ctz)
|
||||
|
||||
Add to your calendar
|
||||
[Event calendar invite](https://calendar.google.com/calendar/event?action=TEMPLATE&tmeid=MDUydWo1dHV1dHFtNzJuM3E0bmEyMW12ZnZfMjAyNTEwMTVUMTYzMDAwWiBjXzljNWVjODhhMmQyYWU5YjY5Mzk4ODU1MGZkNDA5MjVmYjgxYjM4MTI1NGNjYTgzNmRkMjMwNzRiMjNmYzcyZDVAZw&tmsrc=c_9c5ec88a2d2ae9b693988550fd40925fb81b381254cca836dd23074b23fc72d5%40group.calendar.google.com), [.ics file](https://calendar.google.com/calendar/ical/c_9c5ec88a2d2ae9b693988550fd40925fb81b381254cca836dd23074b23fc72d5%40group.calendar.google.com/public/basic.ics), [ADK community calendar](https://calendar.google.com/calendar/embed?src=c_9c5ec88a2d2ae9b693988550fd40925fb81b381254cca836dd23074b23fc72d5%40group.calendar.google.com&ctz=America%2FLos_Angeles), [ADK Community Call RSVP](https://google.qualtrics.com/jfe/form/SV_3K0RJZ64H1BexqS)
|
||||
|
||||
Agenda:
|
||||
[Julia] ADK Roadmap
|
||||
[ Bo & Hangfei] Eng Deep Dive: Context Caching
|
||||
[Kris] How to Contribute
|
||||
|
||||
[Shubham] Upcoming Events
|
||||
|
||||
---
|
||||
|
||||
## 🔥 What's new
|
||||
|
||||
- **Agent Config**: Build agents without code. Check out the
|
||||
[Agent Config](https://google.github.io/adk-docs/agents/config/) feature.
|
||||
- **Context compaction**: Supports context compaction to reduce context length. Here is a [sample](https://github.com/google/adk-python/blob/main/contributing/samples/hello_world_app/agent.py#L156) and [compaction config](https://github.com/google/adk-python/blob/main/src/google/adk/apps/app.py#L51).
|
||||
|
||||
- **Tool Confirmation**: A [tool confirmation flow(HITL)](https://google.github.io/adk-docs/tools/confirmation/) that can guard tool execution with explicit confirmation and custom input
|
||||
- **Resumability**: Support pause and resume an invocation in ADK.
|
||||
|
||||
- **ReflectRetryToolPlugin**: Add [`ReflectRetryToolPlugin`](https://github.com/google/adk-python/blob/main/src/google/adk/plugins/reflect_retry_tool_plugin.py) to reflect from errors and retry with different arguments when tool errors.
|
||||
|
||||
- **Search tool**: Support using Google built-in search and built-in `VertexAiSearchTool` with other tools in the same agent.
|
||||
|
||||
## ✨ Key Features
|
||||
|
||||
@@ -43,6 +73,11 @@ Agent Development Kit (ADK) is a flexible and modular framework for developing a
|
||||
- **Code-First Development**: Define agent logic, tools, and orchestration
|
||||
directly in Python for ultimate flexibility, testability, and versioning.
|
||||
|
||||
- **Agent Config**: Build agents without code. Check out the
|
||||
[Agent Config](https://google.github.io/adk-docs/agents/config/) feature.
|
||||
|
||||
- **Tool Confirmation**: A [tool confirmation flow(HITL)](https://google.github.io/adk-docs/tools/confirmation/) that can guard tool execution with explicit confirmation and custom input.
|
||||
|
||||
- **Modular Multi-Agent Systems**: Design scalable applications by composing
|
||||
multiple specialized agents into flexible hierarchies.
|
||||
|
||||
|
||||
@@ -64,7 +64,9 @@ Always reference this schema when creating configurations to ensure compliance.
|
||||
- **MANDATORY CONFIRMATION**: Say "Please confirm what model you want to use" - do NOT assume or suggest defaults
|
||||
- **EXAMPLES**: "gemini-2.5-flash", "gemini-2.5-pro", etc.
|
||||
- **RATIONALE**: Only LlmAgent requires model specification; workflow agents do not
|
||||
- **DEFAULT ONLY**: Use "{default_model}" only if user explicitly says "use default" or similar
|
||||
- **DEFAULT MODEL**: If user says "use default" or "proceed with default model", use: {default_model}
|
||||
* This is the actual model name, NOT the literal string "default"
|
||||
* The default model for this session is: {default_model}
|
||||
- **WORKFLOW**: Complete all Discovery steps (including this model selection) → Then proceed to Design Phase with model already chosen
|
||||
|
||||
### 2. Design Phase
|
||||
@@ -127,6 +129,43 @@ Always reference this schema when creating configurations to ensure compliance.
|
||||
* **Remember**: Always extract just the folder name after the last slash/separator
|
||||
- No function declarations in YAML (handled automatically by ADK)
|
||||
|
||||
**🚨 CRITICAL: Built-in Tools vs Custom Tools**
|
||||
|
||||
**ADK Built-in Tools** (use directly, NO custom Python file needed):
|
||||
- **Naming**: Use simple name WITHOUT dots (e.g., `google_search`, NOT `google.adk.tools.google_search`)
|
||||
- **No custom code**: Do NOT create Python files for built-in tools
|
||||
- **Available built-in tools**:
|
||||
* `google_search` - Google Search tool
|
||||
* `enterprise_web_search` - Enterprise web search
|
||||
* `google_maps_grounding` - Google Maps grounding
|
||||
* `url_context` - URL context fetching
|
||||
* `VertexAiSearchTool` - Vertex AI Search (class name)
|
||||
* `exit_loop` - Exit loop control
|
||||
* `get_user_choice` - User choice interaction
|
||||
* `load_artifacts` - Load artifacts
|
||||
* `load_memory` - Load memory
|
||||
* `preload_memory` - Preload memory
|
||||
* `transfer_to_agent` - Transfer to another agent
|
||||
|
||||
**Example - Built-in Tool Usage (CORRECT):**
|
||||
```yaml
|
||||
tools:
|
||||
- name: google_search
|
||||
- name: url_context
|
||||
```
|
||||
|
||||
**Example - Built-in Tool Usage (WRONG):**
|
||||
```yaml
|
||||
tools:
|
||||
- name: cb.tools.google_search_tool.google_search_tool # ❌ WRONG - treating built-in as custom
|
||||
```
|
||||
**DO NOT create Python files like `tools/google_search_tool.py` for built-in tools!**
|
||||
|
||||
**Custom Tools** (require Python implementation):
|
||||
- **Naming**: Use dotted path: `{{project_folder_name}}.tools.{{module_name}}.{{function_name}}`
|
||||
- **Require Python file**: Must create actual Python file in `tools/` directory
|
||||
- **Example**: `my_project.tools.dice_tool.roll_dice` → requires `tools/dice_tool.py` with `roll_dice()` function
|
||||
|
||||
**TOOL IMPLEMENTATION STRATEGY:**
|
||||
- **For simple/obvious tools**: Implement them directly with actual working code
|
||||
* Example: dice rolling, prime checking, basic math, file operations
|
||||
@@ -286,11 +325,11 @@ from typing import Optional
|
||||
from google.genai import types
|
||||
from google.adk.agents.callback_context import CallbackContext
|
||||
|
||||
def content_filter_callback(context: CallbackContext) -> Optional[types.Content]:
|
||||
def content_filter_callback(callback_context: CallbackContext) -> Optional[types.Content]:
|
||||
"""After agent callback to filter sensitive content."""
|
||||
# Access the response content through context
|
||||
if hasattr(context, 'response') and context.response:
|
||||
response_text = str(context.response)
|
||||
# Access the response content through callback_context
|
||||
if hasattr(callback_context, 'response') and callback_context.response:
|
||||
response_text = str(callback_context.response)
|
||||
if "confidential" in response_text.lower():
|
||||
filtered_text = response_text.replace("confidential", "[FILTERED]")
|
||||
return types.Content(parts=[types.Part(text=filtered_text)])
|
||||
@@ -306,12 +345,12 @@ from google.adk.models.llm_request import LlmRequest
|
||||
from google.adk.models.llm_response import LlmResponse
|
||||
from google.adk.agents.callback_context import CallbackContext
|
||||
|
||||
def log_model_request(context: CallbackContext, request: LlmRequest) -> Optional[LlmResponse]:
|
||||
def log_model_request(callback_context: CallbackContext, request: LlmRequest) -> Optional[LlmResponse]:
|
||||
"""Before model callback to log requests."""
|
||||
print(f"Model request: {{request.contents}}")
|
||||
return None # Return None to proceed with original request
|
||||
|
||||
def modify_model_response(context: CallbackContext, response: LlmResponse) -> Optional[LlmResponse]:
|
||||
def modify_model_response(callback_context: CallbackContext, response: LlmResponse) -> Optional[LlmResponse]:
|
||||
"""After model callback to modify response."""
|
||||
# Modify response if needed
|
||||
return response # Return modified response or None for original
|
||||
@@ -325,25 +364,25 @@ from typing import Any, Dict, Optional
|
||||
from google.adk.tools.base_tool import BaseTool
|
||||
from google.adk.tools.tool_context import ToolContext
|
||||
|
||||
def validate_tool_input(tool: BaseTool, args: Dict[str, Any], context: ToolContext) -> Optional[Dict]:
|
||||
def validate_tool_input(tool: BaseTool, tool_args: Dict[str, Any], tool_context: ToolContext) -> Optional[Dict]:
|
||||
"""Before tool callback to validate input."""
|
||||
# Validate or modify tool arguments
|
||||
if "unsafe_param" in args:
|
||||
del args["unsafe_param"]
|
||||
return args # Return modified args or None for original
|
||||
if "unsafe_param" in tool_args:
|
||||
del tool_args["unsafe_param"]
|
||||
return tool_args # Return modified args or None for original
|
||||
|
||||
def log_tool_result(tool: BaseTool, args: Dict[str, Any], context: ToolContext, result: Dict) -> Optional[Dict]:
|
||||
def log_tool_result(tool: BaseTool, tool_args: Dict[str, Any], tool_context: ToolContext, result: Dict) -> Optional[Dict]:
|
||||
"""After tool callback to log results."""
|
||||
print(f"Tool {{tool.name}} executed with result: {{result}}")
|
||||
return None # Return None to keep original result
|
||||
```
|
||||
|
||||
## Callback Signature Summary:
|
||||
- **Agent Callbacks**: `(CallbackContext) -> Optional[types.Content]`
|
||||
- **Before Model**: `(CallbackContext, LlmRequest) -> Optional[LlmResponse]`
|
||||
- **After Model**: `(CallbackContext, LlmResponse) -> Optional[LlmResponse]`
|
||||
- **Before Tool**: `(BaseTool, Dict[str, Any], ToolContext) -> Optional[Dict]`
|
||||
- **After Tool**: `(BaseTool, Dict[str, Any], ToolContext, Dict) -> Optional[Dict]`
|
||||
- **Agent Callbacks**: `(callback_context: CallbackContext) -> Optional[types.Content]`
|
||||
- **Before Model**: `(callback_context: CallbackContext, request: LlmRequest) -> Optional[LlmResponse]`
|
||||
- **After Model**: `(callback_context: CallbackContext, response: LlmResponse) -> Optional[LlmResponse]`
|
||||
- **Before Tool**: `(tool: BaseTool, tool_args: Dict[str, Any], tool_context: ToolContext) -> Optional[Dict]`
|
||||
- **After Tool**: `(tool: BaseTool, tool_args: Dict[str, Any], tool_context: ToolContext, result: Dict) -> Optional[Dict]`
|
||||
|
||||
## Important ADK Requirements
|
||||
|
||||
@@ -369,7 +408,7 @@ def log_tool_result(tool: BaseTool, args: Dict[str, Any], context: ToolContext,
|
||||
**ADK AgentConfig Schema Compliance:**
|
||||
- Always reference the embedded ADK AgentConfig schema to verify field requirements
|
||||
- **MODEL FIELD RULES**:
|
||||
* **LlmAgent**: `model` field is REQUIRED (unless inherited from ancestor) - Ask user for preference only when LlmAgent is needed, use "{default_model}" if not specified
|
||||
* **LlmAgent**: `model` field is REQUIRED (unless inherited from ancestor) - Ask user for preference only when LlmAgent is needed, use {default_model} if user says to use default
|
||||
* **Workflow Agents**: `model` field is FORBIDDEN - Remove model field entirely for Sequential/Parallel/Loop agents
|
||||
- Optional fields: description, instruction, tools, sub_agents as defined in ADK AgentConfig schema
|
||||
|
||||
@@ -404,7 +443,6 @@ def log_tool_result(tool: BaseTool, args: Dict[str, Any], context: ToolContext,
|
||||
2. No redundant suggest_file_path calls for pre-approved paths
|
||||
3. Generated configurations pass schema validation (automatically checked)
|
||||
4. Follow ADK naming and organizational conventions
|
||||
5. Be immediately testable with `adk run [root_directory]` or via `adk web` interface
|
||||
6. Include clear, actionable instructions for each agent
|
||||
7. Use appropriate tools for intended functionality
|
||||
|
||||
@@ -424,14 +462,3 @@ def log_tool_result(tool: BaseTool, args: Dict[str, Any], context: ToolContext,
|
||||
6. **Focus on collaboration** - Ensure user gets exactly what they need with clear understanding
|
||||
|
||||
**This workflow eliminates inefficiencies and ensures users get well-organized, predictable file structures in their chosen location.**
|
||||
|
||||
## Running Generated Agents
|
||||
|
||||
**Correct ADK Commands:**
|
||||
- `adk run [project_directory]` - Run agent from project directory (e.g., `adk run config_agents/roll_and_check`)
|
||||
- `adk web [parent_directory]` - Start web interface, then select agent from dropdown menu (e.g., `adk web config_agents`)
|
||||
- **Key Rule**: Always use the project directory for `adk run`, and parent directory for `adk web`
|
||||
|
||||
**Incorrect Commands to Avoid:**
|
||||
- `adk run [project_directory]/root_agent.yaml` - Do NOT specify the YAML file directly
|
||||
- `adk web` without parent directory - Must specify the parent folder containing the agent projects
|
||||
@@ -14,16 +14,20 @@
|
||||
|
||||
"""Cleanup unused files tool for Agent Builder Assistant."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
|
||||
from google.adk.tools.tool_context import ToolContext
|
||||
|
||||
from ..utils.resolve_root_directory import resolve_file_path
|
||||
from ..utils.resolve_root_directory import resolve_file_paths
|
||||
|
||||
|
||||
async def cleanup_unused_files(
|
||||
root_directory: str,
|
||||
used_files: List[str],
|
||||
tool_context: ToolContext,
|
||||
file_patterns: Optional[List[str]] = None,
|
||||
exclude_patterns: Optional[List[str]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
@@ -31,27 +35,32 @@ async def cleanup_unused_files(
|
||||
|
||||
This tool helps clean up unused tool files when agent configurations change.
|
||||
It identifies files that match patterns but aren't referenced in used_files
|
||||
list.
|
||||
list. Paths are resolved automatically using the tool context.
|
||||
|
||||
Args:
|
||||
root_directory: Root directory to scan for unused files
|
||||
used_files: List of file paths currently in use (should not be deleted)
|
||||
tool_context: Tool execution context (provides session state)
|
||||
file_patterns: List of glob patterns to match files (default: ["*.py"])
|
||||
exclude_patterns: List of patterns to exclude (default: ["__init__.py"])
|
||||
|
||||
Returns:
|
||||
Dict containing cleanup results:
|
||||
- success: bool indicating if scan succeeded
|
||||
- root_directory: absolute path to scanned directory
|
||||
- unused_files: list of unused files found
|
||||
- deleted_files: list of files actually deleted
|
||||
- backup_files: list of backup files created
|
||||
- errors: list of error messages
|
||||
- total_freed_space: total bytes freed by deletions
|
||||
"""
|
||||
session_state = tool_context.state
|
||||
root_path = resolve_file_path(".", session_state)
|
||||
|
||||
try:
|
||||
root_path = Path(root_directory).resolve()
|
||||
used_files_set = {Path(f).resolve() for f in used_files}
|
||||
root_path = root_path.resolve()
|
||||
resolved_used_files = {
|
||||
path.resolve()
|
||||
for path in resolve_file_paths(used_files or [], session_state)
|
||||
}
|
||||
|
||||
# Set defaults
|
||||
if file_patterns is None:
|
||||
@@ -61,7 +70,6 @@ async def cleanup_unused_files(
|
||||
|
||||
result = {
|
||||
"success": False,
|
||||
"root_directory": str(root_path),
|
||||
"unused_files": [],
|
||||
"deleted_files": [],
|
||||
"backup_files": [],
|
||||
@@ -85,7 +93,7 @@ async def cleanup_unused_files(
|
||||
# Identify unused files
|
||||
unused_files = []
|
||||
for file_path in all_files:
|
||||
if file_path not in used_files_set:
|
||||
if file_path.resolve() not in resolved_used_files:
|
||||
unused_files.append(file_path)
|
||||
|
||||
result["unused_files"] = [str(f) for f in unused_files]
|
||||
@@ -99,7 +107,6 @@ async def cleanup_unused_files(
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"root_directory": root_directory,
|
||||
"unused_files": [],
|
||||
"deleted_files": [],
|
||||
"backup_files": [],
|
||||
|
||||
@@ -85,7 +85,15 @@ root_agent = Agent(
|
||||
- Use active voice phrasing in your doc updates.
|
||||
- Use second person "you" form of address in your doc updates.
|
||||
9. Create pull requests to update the ADK doc file using the `create_pull_request_from_changes` tool.
|
||||
- For each recommended change, create a separate pull request.
|
||||
- For each recommended change, create a separate pull request. Make sure the recommended change has exactly one pull request.
|
||||
For example, if the ADK doc issue contains the following 2 recommended changes:
|
||||
```
|
||||
1. Title of recommended change 1
|
||||
<content of recommended change 1>
|
||||
2. Title of recommended change 2
|
||||
<content of recommended change 2>
|
||||
```
|
||||
Then you should create 2 pull requests, one for each recommended change, even if each recommended change needs to update multiple ADK doc files.
|
||||
- The title of the pull request should be "Update ADK doc according to issue #<issue number> - <change id>", where <issue number> is the number of the ADK docs issue and <change id> is the id of the recommended change (e.g. "1", "2", etc.).
|
||||
- The body of the pull request should be the instructions about how to update the ADK docs.
|
||||
- **{APPROVAL_INSTRUCTION}**
|
||||
@@ -93,6 +101,7 @@ root_agent = Agent(
|
||||
# 4. Guidelines & Rules
|
||||
- **File Paths:** Always use absolute paths when calling the tools to read files, list directories, or search the codebase.
|
||||
- **Tool Call Parallelism:** Execute multiple independent tool calls in parallel when feasible (i.e. searching the codebase).
|
||||
- **Avoid deletion:** Do not delete any existing content unless specifically directed to do so.
|
||||
- **Explaination:** Provide concise explanations for your actions and reasoning for each step.
|
||||
- **Minimize changes:** When making updates to documentation pages, make the minimum amount of changes to achieve the communication goal. Only make changes that are necessary, and leave everything else as-is.
|
||||
- **Avoid trivial code sample changes:** Update code samples only when adding or modifying functionality. Do not reformat code samples, change variable names, or change code syntax unless you are specifically directed to make those updates.
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from adk_pr_triaging_agent.settings import BOT_LABEL
|
||||
from adk_pr_triaging_agent.settings import GITHUB_BASE_URL
|
||||
from adk_pr_triaging_agent.settings import IS_INTERACTIVE
|
||||
from adk_pr_triaging_agent.settings import OWNER
|
||||
@@ -70,8 +69,10 @@ def get_pull_request_details(pr_number: int) -> str:
|
||||
repository(owner: $owner, name: $repo) {
|
||||
pullRequest(number: $prNumber) {
|
||||
id
|
||||
number
|
||||
title
|
||||
body
|
||||
state
|
||||
author {
|
||||
login
|
||||
}
|
||||
@@ -178,7 +179,7 @@ def add_label_and_reviewer_to_pr(pr_number: int, label: str) -> dict[str, Any]:
|
||||
label_url = (
|
||||
f"{GITHUB_BASE_URL}/repos/{OWNER}/{REPO}/issues/{pr_number}/labels"
|
||||
)
|
||||
label_payload = [label, BOT_LABEL]
|
||||
label_payload = [label]
|
||||
|
||||
try:
|
||||
response = post_request(label_url, label_payload)
|
||||
@@ -299,7 +300,10 @@ root_agent = Agent(
|
||||
# 4. Steps
|
||||
When you are given a PR, here are the steps you should take:
|
||||
- Call the `get_pull_request_details` tool to get the details of the PR.
|
||||
- Skip the PR (i.e. do not label or comment) if the PR is closed or is labeled with "{BOT_LABEL}" or "google-contributior".
|
||||
- Skip the PR (i.e. do not label or comment) if any of the following is true:
|
||||
- the PR is closed
|
||||
- the PR is labeled with "google-contributior"
|
||||
- the PR is already labelled with the above labels (e.g. "documentation", "services", "tools", etc.) and has a reviewer assigned.
|
||||
- Check if the PR is following the contribution guidelines.
|
||||
- If it's not following the guidelines, recommend or add a comment to the PR that points to the contribution guidelines (https://github.com/google/adk-python/blob/main/CONTRIBUTING.md).
|
||||
- If it's following the guidelines, recommend or add a label to the PR.
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
# limitations under the License.
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import time
|
||||
|
||||
from adk_pr_triaging_agent import agent
|
||||
@@ -21,11 +22,14 @@ from adk_pr_triaging_agent.settings import PULL_REQUEST_NUMBER
|
||||
from adk_pr_triaging_agent.settings import REPO
|
||||
from adk_pr_triaging_agent.utils import call_agent_async
|
||||
from adk_pr_triaging_agent.utils import parse_number_string
|
||||
from google.adk.cli.utils import logs
|
||||
from google.adk.runners import InMemoryRunner
|
||||
|
||||
APP_NAME = "adk_pr_triaging_app"
|
||||
USER_ID = "adk_pr_triaging_user"
|
||||
|
||||
logs.setup_adk_logger(level=logging.DEBUG)
|
||||
|
||||
|
||||
async def main():
|
||||
runner = InMemoryRunner(
|
||||
|
||||
@@ -27,7 +27,6 @@ if not GITHUB_TOKEN:
|
||||
|
||||
OWNER = os.getenv("OWNER", "google")
|
||||
REPO = os.getenv("REPO", "adk-python")
|
||||
BOT_LABEL = os.getenv("BOT_LABEL", "bot triaged")
|
||||
PULL_REQUEST_NUMBER = os.getenv("PULL_REQUEST_NUMBER")
|
||||
|
||||
IS_INTERACTIVE = os.environ.get("INTERACTIVE", "1").lower() in ["true", "1"]
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
|
||||
from typing import Any
|
||||
|
||||
from adk_triaging_agent.settings import BOT_LABEL
|
||||
from adk_triaging_agent.settings import GITHUB_BASE_URL
|
||||
from adk_triaging_agent.settings import IS_INTERACTIVE
|
||||
from adk_triaging_agent.settings import OWNER
|
||||
@@ -103,7 +102,7 @@ def add_label_and_owner_to_issue(
|
||||
label_url = (
|
||||
f"{GITHUB_BASE_URL}/repos/{OWNER}/{REPO}/issues/{issue_number}/labels"
|
||||
)
|
||||
label_payload = [label, BOT_LABEL]
|
||||
label_payload = [label]
|
||||
|
||||
try:
|
||||
response = post_request(label_url, label_payload)
|
||||
|
||||
@@ -26,7 +26,6 @@ if not GITHUB_TOKEN:
|
||||
|
||||
OWNER = os.getenv("OWNER", "google")
|
||||
REPO = os.getenv("REPO", "adk-python")
|
||||
BOT_LABEL = os.getenv("BOT_LABEL", "bot triaged")
|
||||
EVENT_NAME = os.getenv("EVENT_NAME")
|
||||
ISSUE_NUMBER = os.getenv("ISSUE_NUMBER")
|
||||
ISSUE_TITLE = os.getenv("ISSUE_TITLE")
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# OAuth Sample
|
||||
|
||||
## Introduction
|
||||
|
||||
This sample data science agent uses Agent Engine Code Execution Sandbox to execute LLM generated code.
|
||||
|
||||
|
||||
## How to use
|
||||
|
||||
* 1. Follow https://cloud.google.com/vertex-ai/generative-ai/docs/agent-engine/code-execution/overview to create a code execution sandbox environment.
|
||||
|
||||
* 2. Replace the SANDBOX_RESOURCE_NAME with the one you just created. If you dont want to create a new sandbox environment directly, the Agent Engine Code Execution Sandbox will create one for you by default using the AGENT_ENGINE_RESOURCE_NAME you specified, however, please ensure to clean up sandboxes after use, otherwise, it will consume quotas.
|
||||
|
||||
|
||||
## Sample prompt
|
||||
|
||||
* Can you write a function that calculates the sum from 1 to 100.
|
||||
* The dataset is given as below. Store,Date,Weekly_Sales,Holiday_Flag,Temperature,Fuel_Price,CPI,Unemployment Store 1,2023-06-01,1000,0,70,3.0,200,5 Store 2,2023-06-02,1200,1,80,3.5,210,6 Store 3,2023-06-03,1400,0,90,4.0,220,7 Store 4,2023-06-04,1600,1,70,4.5,230,8 Store 5,2023-06-05,1800,0,80,5.0,240,9 Store 6,2023-06-06,2000,1,90,5.5,250,10 Store 7,2023-06-07,2200,0,90,6.0,260,11 Plot a scatter plot showcasing the relationship between Weekly Sales and Temperature for each store, distinguishing stores with a Holiday Flag.
|
||||
@@ -0,0 +1,15 @@
|
||||
# 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.
|
||||
|
||||
from . import agent
|
||||
@@ -0,0 +1,95 @@
|
||||
# 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.
|
||||
|
||||
"""Data science agent."""
|
||||
|
||||
from google.adk.agents.llm_agent import Agent
|
||||
from google.adk.code_executors.agent_engine_sandbox_code_executor import AgentEngineSandboxCodeExecutor
|
||||
|
||||
|
||||
def base_system_instruction():
|
||||
"""Returns: data science agent system instruction."""
|
||||
|
||||
return """
|
||||
# Guidelines
|
||||
|
||||
**Objective:** Assist the user in achieving their data analysis goals within the context of a Python Colab notebook, **with emphasis on avoiding assumptions and ensuring accuracy.** Reaching that goal can involve multiple steps. When you need to generate code, you **don't** need to solve the goal in one go. Only generate the next step at a time.
|
||||
|
||||
**Code Execution:** All code snippets provided will be executed within the Colab environment.
|
||||
|
||||
**Statefulness:** All code snippets are executed and the variables stays in the environment. You NEVER need to re-initialize variables. You NEVER need to reload files. You NEVER need to re-import libraries.
|
||||
|
||||
**Output Visibility:** Always print the output of code execution to visualize results, especially for data exploration and analysis. For example:
|
||||
- To look a the shape of a pandas.DataFrame do:
|
||||
```tool_code
|
||||
print(df.shape)
|
||||
```
|
||||
The output will be presented to you as:
|
||||
```tool_outputs
|
||||
(49, 7)
|
||||
|
||||
```
|
||||
- To display the result of a numerical computation:
|
||||
```tool_code
|
||||
x = 10 ** 9 - 12 ** 5
|
||||
print(f'{{x=}}')
|
||||
```
|
||||
The output will be presented to you as:
|
||||
```tool_outputs
|
||||
x=999751168
|
||||
|
||||
```
|
||||
- You **never** generate ```tool_outputs yourself.
|
||||
- You can then use this output to decide on next steps.
|
||||
- Print just variables (e.g., `print(f'{{variable=}}')`.
|
||||
|
||||
**No Assumptions:** **Crucially, avoid making assumptions about the nature of the data or column names.** Base findings solely on the data itself. Always use the information obtained from `explore_df` to guide your analysis.
|
||||
|
||||
**Available files:** Only use the files that are available as specified in the list of available files.
|
||||
|
||||
**Data in prompt:** Some queries contain the input data directly in the prompt. You have to parse that data into a pandas DataFrame. ALWAYS parse all the data. NEVER edit the data that are given to you.
|
||||
|
||||
**Answerability:** Some queries may not be answerable with the available data. In those cases, inform the user why you cannot process their query and suggest what type of data would be needed to fulfill their request.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
root_agent = Agent(
|
||||
model="gemini-2.0-flash-001",
|
||||
name="agent_engine_code_execution_agent",
|
||||
instruction=base_system_instruction() + """
|
||||
|
||||
|
||||
You need to assist the user with their queries by looking at the data and the context in the conversation.
|
||||
You final answer should summarize the code and code execution relevant to the user query.
|
||||
|
||||
You should include all pieces of data to answer the user query, such as the table from code execution results.
|
||||
If you cannot answer the question directly, you should follow the guidelines above to generate the next step.
|
||||
If the question can be answered directly with writing any code, you should do that.
|
||||
If you doesn't have enough data to answer the question, you should ask for clarification from the user.
|
||||
|
||||
You should NEVER install any package on your own like `pip install ...`.
|
||||
When plotting trends, you should make sure to sort and order the data by the x-axis.
|
||||
|
||||
|
||||
""",
|
||||
code_executor=AgentEngineSandboxCodeExecutor(
|
||||
# Replace with your sandbox resource name if you already have one.
|
||||
sandbox_resource_name="SANDBOX_RESOURCE_NAME",
|
||||
# "projects/vertex-agent-loadtest/locations/us-central1/reasoningEngines/6842889780301135872/sandboxEnvironments/6545148628569161728",
|
||||
# Replace with agent engine resource name used for creating sandbox if
|
||||
# sandbox_resource_name is not set.
|
||||
agent_engine_resource_name="AGENT_ENGINE_RESOURCE_NAME",
|
||||
),
|
||||
)
|
||||
@@ -17,7 +17,7 @@ import random
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from google.adk import Agent
|
||||
from google.adk.tools.google_search_tool import google_search
|
||||
from google.adk.tools.google_search_tool import GoogleSearchTool
|
||||
from google.adk.tools.tool_context import ToolContext
|
||||
from google.adk.tools.vertex_ai_search_tool import VertexAiSearchTool
|
||||
|
||||
@@ -57,7 +57,9 @@ root_agent = Agent(
|
||||
""",
|
||||
tools=[
|
||||
roll_die,
|
||||
VertexAiSearchTool(data_store_id=VERTEXAI_DATASTORE_ID),
|
||||
google_search,
|
||||
VertexAiSearchTool(
|
||||
data_store_id=VERTEXAI_DATASTORE_ID, bypass_multi_tools_limit=True
|
||||
),
|
||||
GoogleSearchTool(bypass_multi_tools_limit=True),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
# Computer Use Agent
|
||||
|
||||
This directory contains a computer use agent that can operate a browser to complete user tasks. The agent uses Playwright to control a Chromium browser and can interact with web pages by taking screenshots, clicking, typing, and navigating.
|
||||
|
||||
This agent is to demo the usage of ComputerUseToolset.
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
The computer use agent consists of:
|
||||
- `agent.py`: Main agent configuration using Google's gemini-2.5-computer-use-preview-10-2025 model
|
||||
- `playwright.py`: Playwright-based computer implementation for browser automation
|
||||
- `requirements.txt`: Python dependencies
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Install Python Dependencies
|
||||
|
||||
Install the required Python packages from the requirements file:
|
||||
|
||||
```bash
|
||||
uv pip install -r internal/samples/computer_use/requirements.txt
|
||||
```
|
||||
|
||||
### 2. Install Playwright Dependencies
|
||||
|
||||
Install Playwright's system dependencies for Chromium:
|
||||
|
||||
```bash
|
||||
playwright install-deps chromium
|
||||
```
|
||||
|
||||
### 3. Install Chromium Browser
|
||||
|
||||
Install the Chromium browser for Playwright:
|
||||
|
||||
```bash
|
||||
playwright install chromium
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Running the Agent
|
||||
|
||||
To start the computer use agent, run the following command from the project root:
|
||||
|
||||
```bash
|
||||
adk web internal/samples
|
||||
```
|
||||
|
||||
This will start the ADK web interface where you can interact with the computer_use agent.
|
||||
|
||||
### Example Queries
|
||||
|
||||
Once the agent is running, you can send queries like:
|
||||
|
||||
```
|
||||
find me a flight from SF to Hawaii on next Monday, coming back on next Friday. start by navigating directly to flights.google.com
|
||||
```
|
||||
|
||||
The agent will:
|
||||
1. Open a browser window
|
||||
2. Navigate to the specified website
|
||||
3. Interact with the page elements to complete your task
|
||||
4. Provide updates on its progress
|
||||
|
||||
### Other Example Tasks
|
||||
|
||||
- Book hotel reservations
|
||||
- Search for products online
|
||||
- Fill out forms
|
||||
- Navigate complex websites
|
||||
- Research information across multiple pages
|
||||
|
||||
## Technical Details
|
||||
|
||||
- **Model**: Uses Google's `gemini-2.5-computer-use-preview-10-2025` model for computer use capabilities
|
||||
- **Browser**: Automated Chromium browser via Playwright
|
||||
- **Screen Size**: Configured for 600x800 resolution
|
||||
- **Tools**: Uses ComputerUseToolset for screen capture, clicking, typing, and scrolling
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you encounter issues:
|
||||
|
||||
1. **Playwright not found**: Make sure you've run both `playwright install-deps chromium` and `playwright install chromium`
|
||||
2. **Dependencies missing**: Verify all packages from `requirements.txt` are installed
|
||||
3. **Browser crashes**: Check that your system supports Chromium and has sufficient resources
|
||||
4. **Permission errors**: Ensure your user has permission to run browser automation tools
|
||||
|
||||
## Notes
|
||||
|
||||
- The agent operates in a controlled browser environment
|
||||
- Screenshots are taken to help the agent understand the current state
|
||||
- The agent will provide updates on its actions as it works
|
||||
- Be patient as complex tasks may take some time to complete
|
||||
Executable
+35
@@ -0,0 +1,35 @@
|
||||
# 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.
|
||||
|
||||
from google.adk import Agent
|
||||
from google.adk.models.google_llm import Gemini
|
||||
from google.adk.tools.computer_use.computer_use_toolset import ComputerUseToolset
|
||||
from typing_extensions import override
|
||||
|
||||
from .playwright import PlaywrightComputer
|
||||
|
||||
root_agent = Agent(
|
||||
model='gemini-2.5-computer-use-preview-10-2025',
|
||||
name='hello_world_agent',
|
||||
description=(
|
||||
'computer use agent that can operate a browser on a computer to finish'
|
||||
' user tasks'
|
||||
),
|
||||
instruction="""
|
||||
you are a computer use agent
|
||||
""",
|
||||
tools=[
|
||||
ComputerUseToolset(computer=PlaywrightComputer(screen_size=(1280, 936)))
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,317 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
import asyncio
|
||||
import time
|
||||
from typing import Literal
|
||||
|
||||
from google.adk.tools.computer_use.base_computer import BaseComputer
|
||||
from google.adk.tools.computer_use.base_computer import ComputerEnvironment
|
||||
from google.adk.tools.computer_use.base_computer import ComputerState
|
||||
from playwright.async_api import async_playwright
|
||||
import termcolor
|
||||
from typing_extensions import override
|
||||
|
||||
# Define a mapping from the user-friendly key names to Playwright's expected key names.
|
||||
# Playwright is generally good with case-insensitivity for these, but it's best to be canonical.
|
||||
# See: https://playwright.dev/docs/api/class-keyboard#keyboard-press
|
||||
# Keys like 'a', 'b', '1', '$' are passed directly.
|
||||
PLAYWRIGHT_KEY_MAP = {
|
||||
"backspace": "Backspace",
|
||||
"tab": "Tab",
|
||||
"return": "Enter", # Playwright uses 'Enter'
|
||||
"enter": "Enter",
|
||||
"shift": "Shift",
|
||||
"control": "Control", # Or 'ControlOrMeta' for cross-platform Ctrl/Cmd
|
||||
"alt": "Alt",
|
||||
"escape": "Escape",
|
||||
"space": "Space", # Can also just be " "
|
||||
"pageup": "PageUp",
|
||||
"pagedown": "PageDown",
|
||||
"end": "End",
|
||||
"home": "Home",
|
||||
"left": "ArrowLeft",
|
||||
"up": "ArrowUp",
|
||||
"right": "ArrowRight",
|
||||
"down": "ArrowDown",
|
||||
"insert": "Insert",
|
||||
"delete": "Delete",
|
||||
"semicolon": ";", # For actual character ';'
|
||||
"equals": "=", # For actual character '='
|
||||
"multiply": "Multiply", # NumpadMultiply
|
||||
"add": "Add", # NumpadAdd
|
||||
"separator": "Separator", # Numpad specific
|
||||
"subtract": "Subtract", # NumpadSubtract, or just '-' for character
|
||||
"decimal": "Decimal", # NumpadDecimal, or just '.' for character
|
||||
"divide": "Divide", # NumpadDivide, or just '/' for character
|
||||
"f1": "F1",
|
||||
"f2": "F2",
|
||||
"f3": "F3",
|
||||
"f4": "F4",
|
||||
"f5": "F5",
|
||||
"f6": "F6",
|
||||
"f7": "F7",
|
||||
"f8": "F8",
|
||||
"f9": "F9",
|
||||
"f10": "F10",
|
||||
"f11": "F11",
|
||||
"f12": "F12",
|
||||
"command": "Meta", # 'Meta' is Command on macOS, Windows key on Windows
|
||||
}
|
||||
|
||||
|
||||
class PlaywrightComputer(BaseComputer):
|
||||
"""Conputer that controls Chromium via Playwright."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
screen_size: tuple[int, int],
|
||||
initial_url: str = "https://www.google.com",
|
||||
search_engine_url: str = "https://www.google.com",
|
||||
highlight_mouse: bool = False,
|
||||
):
|
||||
self._initial_url = initial_url
|
||||
self._screen_size = screen_size
|
||||
self._search_engine_url = search_engine_url
|
||||
self._highlight_mouse = highlight_mouse
|
||||
|
||||
@override
|
||||
async def initialize(self):
|
||||
print("Creating session...")
|
||||
self._playwright = await async_playwright().start()
|
||||
self._browser = await self._playwright.chromium.launch(
|
||||
args=["--disable-blink-features=AutomationControlled"],
|
||||
headless=False,
|
||||
)
|
||||
self._context = await self._browser.new_context(
|
||||
viewport={
|
||||
"width": self._screen_size[0],
|
||||
"height": self._screen_size[1],
|
||||
}
|
||||
)
|
||||
self._page = await self._context.new_page()
|
||||
await self._page.goto(self._initial_url)
|
||||
|
||||
termcolor.cprint(
|
||||
f"Started local playwright.",
|
||||
color="green",
|
||||
attrs=["bold"],
|
||||
)
|
||||
|
||||
@override
|
||||
async def environment(self):
|
||||
return ComputerEnvironment.ENVIRONMENT_BROWSER
|
||||
|
||||
@override
|
||||
async def close(self, exc_type, exc_val, exc_tb):
|
||||
if self._context:
|
||||
self._context.close()
|
||||
try:
|
||||
self._browser.close()
|
||||
except Exception as e:
|
||||
# Browser was already shut down because of SIGINT or such.
|
||||
if (
|
||||
"Browser.close: Connection closed while reading from the driver"
|
||||
in str(e)
|
||||
):
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
|
||||
self._playwright.stop()
|
||||
|
||||
async def open_web_browser(self) -> ComputerState:
|
||||
return await self.current_state()
|
||||
|
||||
async def click_at(self, x: int, y: int):
|
||||
await self.highlight_mouse(x, y)
|
||||
await self._page.mouse.click(x, y)
|
||||
await self._page.wait_for_load_state()
|
||||
return await self.current_state()
|
||||
|
||||
async def hover_at(self, x: int, y: int):
|
||||
await self.highlight_mouse(x, y)
|
||||
await self._page.mouse.move(x, y)
|
||||
await self._page.wait_for_load_state()
|
||||
return await self.current_state()
|
||||
|
||||
async def type_text_at(
|
||||
self,
|
||||
x: int,
|
||||
y: int,
|
||||
text: str,
|
||||
press_enter: bool = True,
|
||||
clear_before_typing: bool = True,
|
||||
) -> ComputerState:
|
||||
await self.highlight_mouse(x, y)
|
||||
await self._page.mouse.click(x, y)
|
||||
await self._page.wait_for_load_state()
|
||||
|
||||
if clear_before_typing:
|
||||
await self.key_combination(["Control", "A"])
|
||||
await self.key_combination(["Delete"])
|
||||
|
||||
await self._page.keyboard.type(text)
|
||||
await self._page.wait_for_load_state()
|
||||
|
||||
if press_enter:
|
||||
await self.key_combination(["Enter"])
|
||||
await self._page.wait_for_load_state()
|
||||
return await self.current_state()
|
||||
|
||||
async def _horizontal_document_scroll(
|
||||
self, direction: Literal["left", "right"]
|
||||
) -> ComputerState:
|
||||
# Scroll by 50% of the viewport size.
|
||||
horizontal_scroll_amount = await self.screen_size()[0] // 2
|
||||
if direction == "left":
|
||||
sign = "-"
|
||||
else:
|
||||
sign = ""
|
||||
scroll_argument = f"{sign}{horizontal_scroll_amount}"
|
||||
# Scroll using JS.
|
||||
await self._page.evaluate(f"window.scrollBy({scroll_argument}, 0); ")
|
||||
await self._page.wait_for_load_state()
|
||||
return await self.current_state()
|
||||
|
||||
async def scroll_document(
|
||||
self, direction: Literal["up", "down", "left", "right"]
|
||||
) -> ComputerState:
|
||||
if direction == "down":
|
||||
return await self.key_combination(["PageDown"])
|
||||
elif direction == "up":
|
||||
return await self.key_combination(["PageUp"])
|
||||
elif direction in ("left", "right"):
|
||||
return await self._horizontal_document_scroll(direction)
|
||||
else:
|
||||
raise ValueError("Unsupported direction: ", direction)
|
||||
|
||||
async def scroll_at(
|
||||
self,
|
||||
x: int,
|
||||
y: int,
|
||||
direction: Literal["up", "down", "left", "right"],
|
||||
magnitude: int,
|
||||
) -> ComputerState:
|
||||
await self.highlight_mouse(x, y)
|
||||
|
||||
await self._page.mouse.move(x, y)
|
||||
await self._page.wait_for_load_state()
|
||||
|
||||
dx = 0
|
||||
dy = 0
|
||||
if direction == "up":
|
||||
dy = -magnitude
|
||||
elif direction == "down":
|
||||
dy = magnitude
|
||||
elif direction == "left":
|
||||
dx = -magnitude
|
||||
elif direction == "right":
|
||||
dx = magnitude
|
||||
else:
|
||||
raise ValueError("Unsupported direction: ", direction)
|
||||
|
||||
await self._page.mouse.wheel(dx, dy)
|
||||
await self._page.wait_for_load_state()
|
||||
return await self.current_state()
|
||||
|
||||
async def wait(self, seconds: int) -> ComputerState:
|
||||
await asyncio.sleep(seconds)
|
||||
return await self.current_state()
|
||||
|
||||
async def go_back(self) -> ComputerState:
|
||||
await self._page.go_back()
|
||||
await self._page.wait_for_load_state()
|
||||
return await self.current_state()
|
||||
|
||||
async def go_forward(self) -> ComputerState:
|
||||
await self._page.go_forward()
|
||||
await self._page.wait_for_load_state()
|
||||
return await self.current_state()
|
||||
|
||||
async def search(self) -> ComputerState:
|
||||
return await self.navigate(self._search_engine_url)
|
||||
|
||||
async def navigate(self, url: str) -> ComputerState:
|
||||
await self._page.goto(url)
|
||||
await self._page.wait_for_load_state()
|
||||
return await self.current_state()
|
||||
|
||||
async def key_combination(self, keys: list[str]) -> ComputerState:
|
||||
# Normalize all keys to the Playwright compatible version.
|
||||
keys = [PLAYWRIGHT_KEY_MAP.get(k.lower(), k) for k in keys]
|
||||
|
||||
for key in keys[:-1]:
|
||||
await self._page.keyboard.down(key)
|
||||
|
||||
await self._page.keyboard.press(keys[-1])
|
||||
|
||||
for key in reversed(keys[:-1]):
|
||||
await self._page.keyboard.up(key)
|
||||
|
||||
return await self.current_state()
|
||||
|
||||
async def drag_and_drop(
|
||||
self, x: int, y: int, destination_x: int, destination_y: int
|
||||
) -> ComputerState:
|
||||
await self.highlight_mouse(x, y)
|
||||
await self._page.mouse.move(x, y)
|
||||
await self._page.wait_for_load_state()
|
||||
await self._page.mouse.down()
|
||||
await self._page.wait_for_load_state()
|
||||
|
||||
await self.highlight_mouse(destination_x, destination_y)
|
||||
await self._page.mouse.move(destination_x, destination_y)
|
||||
await self._page.wait_for_load_state()
|
||||
await self._page.mouse.up()
|
||||
return await self.current_state()
|
||||
|
||||
async def current_state(self) -> ComputerState:
|
||||
await self._page.wait_for_load_state()
|
||||
# Even if Playwright reports the page as loaded, it may not be so.
|
||||
# Add a manual sleep to make sure the page has finished rendering.
|
||||
time.sleep(0.5)
|
||||
screenshot_bytes = await self._page.screenshot(type="png", full_page=False)
|
||||
return ComputerState(screenshot=screenshot_bytes, url=self._page.url)
|
||||
|
||||
async def screen_size(self) -> tuple[int, int]:
|
||||
return self._screen_size
|
||||
|
||||
async def highlight_mouse(self, x: int, y: int):
|
||||
if not self._highlight_mouse:
|
||||
return
|
||||
await self._page.evaluate(f"""
|
||||
() => {{
|
||||
const element_id = "playwright-feedback-circle";
|
||||
const div = document.createElement('div');
|
||||
div.id = element_id;
|
||||
div.style.pointerEvents = 'none';
|
||||
div.style.border = '4px solid red';
|
||||
div.style.borderRadius = '50%';
|
||||
div.style.width = '20px';
|
||||
div.style.height = '20px';
|
||||
div.style.position = 'fixed';
|
||||
div.style.zIndex = '9999';
|
||||
document.body.appendChild(div);
|
||||
|
||||
div.hidden = false;
|
||||
div.style.left = {x} - 10 + 'px';
|
||||
div.style.top = {y} - 10 + 'px';
|
||||
|
||||
setTimeout(() => {{
|
||||
div.hidden = true;
|
||||
}}, 2000);
|
||||
}}
|
||||
""")
|
||||
# Wait a bit for the user to see the cursor.
|
||||
time.sleep(1)
|
||||
@@ -0,0 +1,4 @@
|
||||
termcolor==3.1.0
|
||||
playwright==1.52.0
|
||||
browserbase==1.3.0
|
||||
rich
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user