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
65 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fb941f9011 | |||
| de44bbc17c | |||
| 7deffb16fd | |||
| ac1401bd44 | |||
| 3c63c2ad39 | |||
| f90adff8c5 | |||
| 1de65cf314 | |||
| 2220d885cd | |||
| 37e6507ce4 | |||
| 004e15ccb7 | |||
| 574ec43a17 | |||
| ce07cd8144 | |||
| 706a6dda81 | |||
| da73e718ef | |||
| 125bc85ac5 | |||
| 6ff10b23be | |||
| f82ceb0ce7 | |||
| 33012e6dda | |||
| 666cebe369 | |||
| dd8cd27b2c | |||
| e87a8437fb | |||
| 7d58e0d2f3 | |||
| 9290b96626 | |||
| 2770012cec | |||
| 131fbd3948 | |||
| 798f65df86 | |||
| ee873cae2e | |||
| fe82f3cde8 | |||
| 798d0053c8 | |||
| d0102ecea3 | |||
| 63a8eba53f | |||
| 47221cd5c1 | |||
| 2ac468ea7e | |||
| 00aba2d884 | |||
| 585ebfdac7 | |||
| c0c98d94b3 | |||
| 288c2c448d | |||
| ecce7e54a6 | |||
| 381d44cab4 | |||
| 4341839420 | |||
| 32f9f92042 | |||
| a16e3cc67e | |||
| 8f7d9659cf | |||
| 3480b3b82d | |||
| 65cbf4b35c | |||
| 4ee125a038 | |||
| 2155a35c51 | |||
| 3bcd8f7f7a | |||
| c34615ecf3 | |||
| 1063fa532c | |||
| 43d6075ea7 | |||
| 553e376718 | |||
| 85434e293f | |||
| 324796b4fe | |||
| 853a3b0e14 | |||
| 714c3ad047 | |||
| 801233902b | |||
| 025b42c836 | |||
| 753084fd46 | |||
| 2380afd287 | |||
| 316463be86 | |||
| 83c24d2e74 | |||
| e8f7aa3140 | |||
| 7edfb2701c | |||
| 0d38a3683f |
@@ -7,34 +7,69 @@ assignees: ''
|
||||
|
||||
---
|
||||
|
||||
** Please make sure you read the contribution guide and file the issues in the right place. **
|
||||
[Contribution guide.](https://google.github.io/adk-docs/contributing-guide/)
|
||||
## đź”´ Required Information
|
||||
*Please ensure all items in this section are completed to allow for efficient
|
||||
triaging. Requests without complete information may be rejected / deprioritized.
|
||||
If an item is not applicable to you - please mark it as N/A*
|
||||
|
||||
**Describe the bug**
|
||||
**Describe the Bug:**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Please share a minimal code and data to reproduce your problem.
|
||||
Steps to reproduce the behavior:
|
||||
**Steps to Reproduce:**
|
||||
Please provide a numbered list of steps to reproduce the behavior:
|
||||
1. Install '...'
|
||||
2. Run '....'
|
||||
3. Open '....'
|
||||
4. Provide error or stacktrace
|
||||
|
||||
**Expected behavior**
|
||||
**Expected Behavior:**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
**Observed Behavior:**
|
||||
What actually happened? Include error messages or crash stack traces here.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. macOS, Linux, Windows]
|
||||
- Python version(python -V):
|
||||
- ADK version(pip show google-adk):
|
||||
**Environment Details:**
|
||||
|
||||
- ADK Library Version (pip show google-adk):
|
||||
- Desktop OS:** [e.g., macOS, Linux, Windows]
|
||||
- Python Version (python -V):
|
||||
|
||||
**Model Information:**
|
||||
|
||||
**Model Information:**
|
||||
- Are you using LiteLLM: Yes/No
|
||||
- Which model is being used(e.g. gemini-2.5-pro)
|
||||
- Which model is being used: (e.g., gemini-2.5-pro)
|
||||
|
||||
**Additional context**
|
||||
---
|
||||
|
||||
## 🟡 Optional Information
|
||||
*Providing this information greatly speeds up the resolution process.*
|
||||
|
||||
**Regression:**
|
||||
Did this work in a previous version of ADK? If so, which one?
|
||||
|
||||
**Logs:**
|
||||
Please attach relevant logs. Wrap them in code blocks (```) or attach a
|
||||
text file.
|
||||
```text
|
||||
// Paste logs here
|
||||
```
|
||||
|
||||
**Screenshots / Video:**
|
||||
If applicable, add screenshots or screen recordings to help explain
|
||||
your problem.
|
||||
|
||||
**Additional Context:**
|
||||
Add any other context about the problem here.
|
||||
|
||||
**Minimal Reproduction Code:**
|
||||
Please provide a code snippet or a link to a Gist/repo that isolates the issue.
|
||||
```python
|
||||
// Code snippet here
|
||||
```
|
||||
|
||||
**How often has this issue occurred?:**
|
||||
|
||||
- Always (100%)
|
||||
- Often (50%+)
|
||||
- Intermittently (<50%)
|
||||
- Once / Rare
|
||||
|
||||
@@ -10,14 +10,39 @@ assignees: ''
|
||||
** Please make sure you read the contribution guide and file the issues in the right place. **
|
||||
[Contribution guide.](https://google.github.io/adk-docs/contributing-guide/)
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
## đź”´ Required Information
|
||||
*Please ensure all items in this section are completed to allow for efficient
|
||||
triaging. Requests without complete information may be rejected / deprioritized.
|
||||
If an item is not applicable to you - please mark it as N/A*
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
### Is your feature request related to a specific problem?
|
||||
Please describe the problem you are trying to solve. (Ex: "I'm always frustrated
|
||||
when I have to manually handle X...")
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
### Describe the Solution You'd Like
|
||||
A clear and concise description of the feature or API change you want.
|
||||
Be specific about input/outputs if this involves an API change.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
### Impact on your work
|
||||
How does this feature impact your work and what are you trying to achieve?
|
||||
If this is critical for you, tell us if there is a timeline by when you need
|
||||
this feature.
|
||||
|
||||
### Willingness to contribute
|
||||
Are you interested in implementing this feature yourself or submitting a PR?
|
||||
(Yes/No)
|
||||
|
||||
---
|
||||
|
||||
## 🟡 Recommended Information
|
||||
|
||||
### Describe Alternatives You've Considered
|
||||
A clear and concise description of any alternative solutions or workarounds
|
||||
you've considered and why they didn't work for you.
|
||||
|
||||
### Proposed API / Implementation
|
||||
If you have ideas on how this should look in code, please share a
|
||||
pseudo-code example.
|
||||
|
||||
### Additional Context
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -0,0 +1,74 @@
|
||||
name: Mypy New Error Check
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
|
||||
jobs:
|
||||
mypy-diff:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.10', '3.11', '3.12', '3.13',]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
|
||||
- name: Generate Baseline (Main)
|
||||
run: |
|
||||
# Switch to main branch to generate baseline
|
||||
git checkout origin/main
|
||||
|
||||
git checkout ${{ github.sha }} -- pyproject.toml
|
||||
|
||||
# Install dependencies for main
|
||||
uv venv .venv
|
||||
source .venv/bin/activate
|
||||
uv sync --all-extras
|
||||
|
||||
# Run mypy, filter for errors only, remove line numbers (file:123: -> file::), and sort
|
||||
# We ignore exit code (|| true) because we expect errors on main
|
||||
uv run mypy . | grep "error:" | sed 's/:\([0-9]\+\):/::/g' | sort > main_errors.txt || true
|
||||
|
||||
echo "Found $(wc -l < main_errors.txt) errors on main."
|
||||
|
||||
- name: Check PR Branch
|
||||
run: |
|
||||
# Switch back to the PR commit
|
||||
git checkout ${{ github.sha }}
|
||||
|
||||
# Re-sync dependencies in case the PR changed them
|
||||
source .venv/bin/activate
|
||||
uv sync --all-extras
|
||||
|
||||
# Run mypy on PR code, apply same processing
|
||||
uv run mypy . | grep "error:" | sed 's/:\([0-9]\+\):/::/g' | sort > pr_errors.txt || true
|
||||
|
||||
echo "Found $(wc -l < pr_errors.txt) errors on PR branch."
|
||||
|
||||
- name: Compare and Fail on New Errors
|
||||
run: |
|
||||
# 'comm -13' suppresses unique lines in file1 (main) and common lines,
|
||||
# leaving only lines unique to file2 (PR) -> The new errors.
|
||||
comm -13 main_errors.txt pr_errors.txt > new_errors.txt
|
||||
|
||||
if [ -s new_errors.txt ]; then
|
||||
echo "::error::The following NEW mypy errors were introduced:"
|
||||
cat new_errors.txt
|
||||
exit 1
|
||||
else
|
||||
echo "Great job! No new mypy errors introduced."
|
||||
fi
|
||||
@@ -0,0 +1,32 @@
|
||||
name: Mypy Type Check
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
mypy:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.10', '3.11', '3.12', '3.13',]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v1
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync --all-extras
|
||||
|
||||
- name: Run mypy
|
||||
|
||||
run: uv run mypy . --strict
|
||||
+86
-1
@@ -1,15 +1,100 @@
|
||||
# Changelog
|
||||
|
||||
## [1.24.0](https://github.com/google/adk-python/compare/v1.23.0...v1.24.0) (2026-02-04)
|
||||
|
||||
### âš BREAKING CHANGES
|
||||
|
||||
* Breaking: Make credential manager accept `tool_context` instead of `callback_context` ([fe82f3c](https://github.com/google/adk-python/commit/fe82f3cde854e49be13d90b4c02d786d82f8a202))
|
||||
|
||||
### Highlights
|
||||
|
||||
* **[Web]**
|
||||
* **Consolidated Event View**: Replaced the Event tab with a more intuitive "click-to-expand" interaction on message rows, enabling faster debugging within the chat context
|
||||
* **Enhanced Accessibility**: Added full support for arrow-key navigation for a more seamless, keyboard-centric experience
|
||||
* **Rich Developer Tooling**: Introduced detailed tooltips for function calls, providing instant visibility into arguments, responses, and state changes
|
||||
* **A2UI Integration**: Integrated the **A2UI v0.8** standard catalog to automatically render spec-compliant ADK parts as native UI components directly in the chat
|
||||
|
||||
### Features
|
||||
|
||||
* **[Core]**
|
||||
* Allow passthrough of `GOOGLE_CLOUD_LOCATION` for Agent Engine deployments ([004e15c](https://github.com/google/adk-python/commit/004e15ccb7c7f683623f8e7d2e77a9d12558c545))
|
||||
* Add interface for agent optimizers ([4ee125a](https://github.com/google/adk-python/commit/4ee125a03856fdb9ed28245bf7f5917c2d9038db))
|
||||
* Pass event ID as metadata when converted into a message ([85434e2](https://github.com/google/adk-python/commit/85434e293f7bd1e3711f190f84d5a36804e4462b))
|
||||
* Restructure the bug report template as per the intake process ([324796b](https://github.com/google/adk-python/commit/324796b4fe05bec3379bfef67071a29552ef355a))
|
||||
|
||||
* **[Models]**
|
||||
* Mark Vertex calls made from non-Gemini models ([7d58e0d](https://github.com/google/adk-python/commit/7d58e0d2f375bc80bdfac9cffea2926fd2344b8a))
|
||||
|
||||
* **[Evals]**
|
||||
* Allow Vertex AI Client initialization with API Key ([43d6075](https://github.com/google/adk-python/commit/43d6075ea7aa49ddb358732f2219ca9598dd286f))
|
||||
* Remove overall evaluation status calculation from `_CustomMetricEvaluator` and add threshold to custom metric function expected signature ([553e376](https://github.com/google/adk-python/commit/553e376718ceb3d7fb1403231bb720836d71f42c))
|
||||
|
||||
* **[Tools]**
|
||||
* Make OpenAPI tool asynchronous ([9290b96](https://github.com/google/adk-python/commit/9290b966267dc02569786f95aab2a3cb78c7004f))
|
||||
* Implement toolset authentication for `McpToolset`, `OpenAPIToolset`, and other toolsets ([798f65d](https://github.com/google/adk-python/commit/798f65df86b1bbe33d864e30c5b1f9e155e89810))
|
||||
* Add framework support for toolset authentication before `get_tools` calls ([ee873ca](https://github.com/google/adk-python/commit/ee873cae2e2df960910d264a4340ce6c0489eb7a))
|
||||
* Support dynamic configuration for `VertexAiSearchTool` ([585ebfd](https://github.com/google/adk-python/commit/585ebfdac7f1b8007b4e4a7e4258ec5de72c78b1))
|
||||
* Add `get_auth_config` method to toolset to expose authentication requirements ([381d44c](https://github.com/google/adk-python/commit/381d44cab437cac027af181ae627e7b260b7561e))
|
||||
* Add methods in `McpToolset` for users to access MCP resources ([8f7d965](https://github.com/google/adk-python/commit/8f7d9659cfc19034af29952fbca765d012169b38))
|
||||
* Improve error message when failing to get tools from MCP ([3480b3b](https://github.com/google/adk-python/commit/3480b3b82d89de69f77637d7ad034827434df45a))
|
||||
|
||||
* **[Services]**
|
||||
* Improve `asyncio` loop handling and test cleanup ([00aba2d](https://github.com/google/adk-python/commit/00aba2d884d24fb5244d1de84f8dba9cbc3c07e8))
|
||||
|
||||
* **[Live]**
|
||||
* Support running tools in separate threads for live mode ([714c3ad](https://github.com/google/adk-python/commit/714c3ad0477e775fba6696a919a366a293197268))
|
||||
|
||||
* **[Observability]**
|
||||
* Add extra attributes to spans generated with `opentelemetry-instrumentation-google-genai` ([e87a843](https://github.com/google/adk-python/commit/e87a8437fb430e0d4c42c73948e3ba1872040a15))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Ignore `session_db_kwargs` for SQLite session services ([ce07cd8](https://github.com/google/adk-python/commit/ce07cd8144c8498434f68e61ebeb519bf329f778))
|
||||
* Resolve `MutualTLSChannelError` by adding `pyopenssl` dependency ([125bc85](https://github.com/google/adk-python/commit/125bc85ac5e1400bc38f7c681f76fa82626c9911))
|
||||
* Add `update_timestamp_tz` property to `StorageSession` ([666cebe](https://github.com/google/adk-python/commit/666cebe3693d2981fd5fea6e9e4c65e56dcd3f2b))
|
||||
* Do not treat Function Calls and Function Responses as invisible when marked as thoughts ([853a3b0](https://github.com/google/adk-python/commit/853a3b0e143ce27516f0de51e0e0df2af6ecf465))
|
||||
* Add pre-deployment validation for agent module imports (credit to @ppgranger, [2ac468e](https://github.com/google/adk-python/commit/2ac468ea7e30ef30c1324ffc86f67dbf32ab7ede))
|
||||
* Fix cases where `execution_result_delimiters` have `None` type element ([a16e3cc](https://github.com/google/adk-python/commit/a16e3cc67e1cb391228ba78662547672404ae550))
|
||||
* Disable `save_input_blobs_as_artifacts` deprecation warning message for users not setting it ([c34615e](https://github.com/google/adk-python/commit/c34615ecf3c7bbe0f4275f72543774f258c565b4))
|
||||
* Fix agent config path handling in generated deployment script ([8012339](https://github.com/google/adk-python/commit/801233902bbd6c0cca63b6fc8c1b0b2531f3f11e))
|
||||
* Add `pypika>=0.50.0` to `project.toml` to support `crewai` on Python 3.12+ ([e8f7aa3](https://github.com/google/adk-python/commit/e8f7aa3140d2585ac38ebfe31c5b650383499a20))
|
||||
* Update OpenTelemetry dependency versions to relax version constraints for `opentelemetry-api` and `opentelemetry-sdk` ([706a6dd](https://github.com/google/adk-python/commit/706a6dda8144da147bd9fa42ef85bbfa58fec5d3))
|
||||
* Enable `pool_pre_ping` by default for non-SQLite database engines ([da73e71](https://github.com/google/adk-python/commit/da73e718efa9557ed33e2fb579de68fcbcf4c7f0))
|
||||
* Ensure database sessions are always rolled back on errors ([63a8eba](https://github.com/google/adk-python/commit/63a8eba53f2cb07625eb7cd111ff767e8e0030fa))
|
||||
* Reload stale session in `DatabaseSessionService` when storage update time is later than the in-memory session object ([1063fa5](https://github.com/google/adk-python/commit/1063fa532cad59d8e9f7421ce2f523724d49d541))
|
||||
* Make credential key generation stable and prevent cross-user credential leaks ([33012e6](https://github.com/google/adk-python/commit/33012e6dda9ef20c7a1dae66a84717de5d782097))
|
||||
* Change MCP `read_resource` to return original contents ([ecce7e5](https://github.com/google/adk-python/commit/ecce7e54a688a915a1b9d742c39e4684186729be))
|
||||
* Recognize function responses as non-empty parts in LiteLLM ([d0102ec](https://github.com/google/adk-python/commit/d0102ecea331e062190dbb7578a4ef7f4044306e))
|
||||
* Handle HTTP/HTTPS URLs for media files in LiteLLM content conversion ([47221cd](https://github.com/google/adk-python/commit/47221cd5c1e778cd4b92ed8d382c639435f5728c))
|
||||
* Fix Pydantic schema generation error for `ClientSession` ([131fbd3](https://github.com/google/adk-python/commit/131fbd39482980572487a30fea13236d2badd543))
|
||||
* Fix Click’s Wrapping in `adk eval` help message ([3bcd8f7](https://github.com/google/adk-python/commit/3bcd8f7f7a0683f838005bc209f7d39dc93f850b))
|
||||
* Stream errors as simple JSON objects in ADK web server SSE endpoint ([798d005](https://github.com/google/adk-python/commit/798d0053c832e7ed52e2e104f8a14f789ba8b17f))
|
||||
* Remove print debugging artifact ([0d38a36](https://github.com/google/adk-python/commit/0d38a3683f13bc12dc5d181164b6cd5d72fc260c))
|
||||
|
||||
### Improvements
|
||||
|
||||
* Check `will_continue` for streaming function calls ([2220d88](https://github.com/google/adk-python/commit/2220d885cda875144b52338b5becf6e5546f3f51))
|
||||
* Update ADK web, rework events, and add A2UI capabilities ([37e6507](https://github.com/google/adk-python/commit/37e6507ce4d8750100d914eb1a62014350ef1795))
|
||||
* Improve error handling for LiteLLM import in `gemma_llm.py` ([574ec43](https://github.com/google/adk-python/commit/574ec43a175e3bf3a05e73114e8db7196fae7040))
|
||||
* Replace proxy methods with utils implementation ([6ff10b2](https://github.com/google/adk-python/commit/6ff10b23be01c1f7dd79d13ac8c679c079140f76), [f82ceb0](https://github.com/google/adk-python/commit/f82ceb0ce75d3efed7c046835ddac76c28210013))
|
||||
* Replace print statements with logging in ADK evaluation components ([dd8cd27](https://github.com/google/adk-python/commit/dd8cd27b2ce505ecca50cdfbb1469db01c82b0af))
|
||||
* Add sample agent that requires OAuth flow during MCP tool listing, and convert `MCPToolset` to `McpToolset` in unit tests ([2770012](https://github.com/google/adk-python/commit/2770012cecdfc71628a818a75b21faabe828b4e5), [4341839](https://github.com/google/adk-python/commit/43418394202c2d01b0d37f6424bd601148077e27))
|
||||
* Ensure `BigQueryAgentAnalyticsPlugin` is shut down after each test ([c0c98d9](https://github.com/google/adk-python/commit/c0c98d94b3161d6bf9fff731e0abfc985b53e653))
|
||||
* Add ADK logger in `RestApiTool` ([288c2c4](https://github.com/google/adk-python/commit/288c2c448d77c574dafadf7851a49e6ff59fa7f4))
|
||||
* Add GitHub Action check to run `mypy` ([32f9f92](https://github.com/google/adk-python/commit/32f9f92042ab530220ac9d159045c91d311affa7))
|
||||
* Add `unittests.sh` script and update `CONTRIBUTING.md` ([025b42c](https://github.com/google/adk-python/commit/025b42c8361ad2078593e3e7fc5301df88a532c7))
|
||||
* Extract helper function for LLM request building and response processing ([753084f](https://github.com/google/adk-python/commit/753084fd46c9637488f33b0a05b4d270f6e03a39))
|
||||
|
||||
## [1.23.0](https://github.com/google/adk-python/compare/v1.22.1...v1.23.0) (2026-01-22)
|
||||
|
||||
### âš BREAKING CHANGES
|
||||
|
||||
* Breaking: Use OpenTelemetry for BigQuery plugin tracing, replacing custom `ContextVar` implementation ([ab89d12](https://github.com/google/adk-python/commit/ab89d1283430041afb303834749869e9ee331721))
|
||||
* Add support to automatically create a session if one does not exist ([8e69a58](https://github.com/google/adk-python/commit/8e69a58df4eadeccbb100b7264bb518a46b61fd7))
|
||||
|
||||
### Features
|
||||
|
||||
* **[Core]**
|
||||
* Add support to automatically create a session if one does not exist ([8e69a58](https://github.com/google/adk-python/commit/8e69a58df4eadeccbb100b7264bb518a46b61fd7))
|
||||
* Remove `@experimental` decorator from `AgentEngineSandboxCodeExecutor` ([135f763](https://github.com/google/adk-python/commit/135f7633253f6a415302142abc3579b664601d5b))
|
||||
* Add `--disable_features` CLI option to override default feature enable state ([53b67ce](https://github.com/google/adk-python/commit/53b67ce6340f3f3f8c3d732f9f7811e445c76359))
|
||||
* Add `otel_to_cloud` flag to `adk deploy agent_engine` command ([21f63f6](https://github.com/google/adk-python/commit/21f63f66ee424501d9a70806277463ef718ae843))
|
||||
|
||||
@@ -188,6 +188,18 @@ part before or alongside your code PR.
|
||||
pytest ./tests/unittests
|
||||
```
|
||||
|
||||
**Alternatively**, use the included `unittests.sh` script which handles
|
||||
environment setup and restoration automatically:
|
||||
|
||||
```shell
|
||||
./scripts/unittests.sh
|
||||
```
|
||||
|
||||
This script will:
|
||||
- Set up the test environment with minimal dependencies (`test`, `eval`, `a2a`)
|
||||
- Run the unit tests
|
||||
- Restore the full development environment (`--all-extras`)
|
||||
|
||||
6. **Auto-format the code:**
|
||||
|
||||
**NOTE**: We use `isort` and `pyink` for styles. Use the included
|
||||
|
||||
@@ -115,7 +115,7 @@ root_agent = Agent(
|
||||
|
||||
### Define a multi-agent system:
|
||||
|
||||
Define a multi-agent system with coordinator agent, greeter agent, and task execution agent. Then ADK engine and the model will guide the agents works together to accomplish the task.
|
||||
Define a multi-agent system with coordinator agent, greeter agent, and task execution agent. Then ADK engine and the model will guide the agents to work together to accomplish the task.
|
||||
|
||||
```python
|
||||
from google.adk.agents import LlmAgent, BaseAgent
|
||||
|
||||
@@ -57,8 +57,25 @@ from google.adk import Agent
|
||||
from google.adk.agents.loop_agent import LoopAgent
|
||||
from google.adk.agents.readonly_context import ReadonlyContext
|
||||
from google.adk.agents.sequential_agent import SequentialAgent
|
||||
from google.adk.models import Gemini
|
||||
from google.adk.tools.exit_loop_tool import exit_loop
|
||||
from google.adk.tools.tool_context import ToolContext
|
||||
from google.genai import types
|
||||
|
||||
# Retry configuration for handling API rate limits and overload
|
||||
_RETRY_OPTIONS = types.HttpRetryOptions(
|
||||
initial_delay=10,
|
||||
attempts=8,
|
||||
exp_base=2,
|
||||
max_delay=300,
|
||||
http_status_codes=[429, 503],
|
||||
)
|
||||
|
||||
# Use gemini-3-pro-preview for planning and summary (better quality)
|
||||
GEMINI_PRO_WITH_RETRY = Gemini(
|
||||
model="gemini-3-pro-preview",
|
||||
retry_options=_RETRY_OPTIONS,
|
||||
)
|
||||
|
||||
# Maximum number of files per analysis group to avoid context overflow
|
||||
MAX_FILES_PER_GROUP = 5
|
||||
@@ -249,7 +266,7 @@ def get_release_context(tool_context: ToolContext) -> dict[str, Any]:
|
||||
# =============================================================================
|
||||
|
||||
planner_agent = Agent(
|
||||
model="gemini-2.5-pro",
|
||||
model=GEMINI_PRO_WITH_RETRY,
|
||||
name="release_planner",
|
||||
description=(
|
||||
"Plans the analysis by fetching release info and organizing files into"
|
||||
@@ -272,10 +289,12 @@ efficient processing.
|
||||
|
||||
3. Call `get_changed_files_summary` to get the list of changed files WITHOUT
|
||||
the full patches (to save context space).
|
||||
- **IMPORTANT**: Pass these parameters:
|
||||
- `local_repo_path="{LOCAL_REPOS_DIR_PATH}/{CODE_REPO}"` to avoid 300-file limit
|
||||
- `path_filter="src/google/adk/"` to only get ADK source files (reduces token usage)
|
||||
|
||||
4. Filter and organize the files:
|
||||
- **INCLUDE** only files in `src/google/adk/` directory
|
||||
- **EXCLUDE** test files, `__init__.py`, and files outside src/
|
||||
4. Further filter the returned files:
|
||||
- **EXCLUDE** test files and `__init__.py` files
|
||||
- **IMPORTANT**: Do NOT exclude any file just because it has few changes.
|
||||
Even single-line changes to public APIs need documentation updates.
|
||||
- **PRIORITIZE** by importance:
|
||||
@@ -421,7 +440,7 @@ files and finding related documentation that needs updating.
|
||||
|
||||
|
||||
file_group_analyzer = Agent(
|
||||
model="gemini-2.5-pro",
|
||||
model=GEMINI_PRO_WITH_RETRY,
|
||||
name="file_group_analyzer",
|
||||
description=(
|
||||
"Analyzes a group of changed files and generates recommendations."
|
||||
@@ -505,7 +524,7 @@ Present a summary of:
|
||||
|
||||
|
||||
summary_agent = Agent(
|
||||
model="gemini-2.5-pro",
|
||||
model=GEMINI_PRO_WITH_RETRY,
|
||||
name="summary_agent",
|
||||
description="Compiles recommendations and creates the GitHub issue.",
|
||||
instruction=summary_instruction,
|
||||
@@ -540,7 +559,7 @@ analysis_pipeline = SequentialAgent(
|
||||
# =============================================================================
|
||||
|
||||
root_agent = Agent(
|
||||
model="gemini-2.5-pro",
|
||||
model=GEMINI_PRO_WITH_RETRY,
|
||||
name="adk_release_analyzer",
|
||||
description=(
|
||||
"Analyzes ADK Python releases and generates documentation update"
|
||||
|
||||
@@ -600,23 +600,41 @@ def get_file_diff_for_release(
|
||||
|
||||
|
||||
def get_changed_files_summary(
|
||||
repo_owner: str, repo_name: str, start_tag: str, end_tag: str
|
||||
repo_owner: str,
|
||||
repo_name: str,
|
||||
start_tag: str,
|
||||
end_tag: str,
|
||||
local_repo_path: Optional[str] = None,
|
||||
path_filter: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Gets a summary of changed files between two releases without patches.
|
||||
|
||||
This is a lighter-weight version of get_changed_files_between_releases
|
||||
that only returns file paths and metadata, without the actual diff content.
|
||||
Use this for planning which files to analyze.
|
||||
This function uses local git commands when local_repo_path is provided,
|
||||
which avoids the GitHub API's 300-file limit for large comparisons.
|
||||
Falls back to GitHub API if local_repo_path is not provided or invalid.
|
||||
|
||||
Args:
|
||||
repo_owner: The name of the repository owner.
|
||||
repo_name: The name of the repository.
|
||||
start_tag: The older tag (base) for the comparison.
|
||||
end_tag: The newer tag (head) for the comparison.
|
||||
local_repo_path: Optional absolute path to local git repo. If provided
|
||||
and valid, uses git diff instead of GitHub API to get complete
|
||||
file list (avoids 300-file limit).
|
||||
path_filter: Optional path prefix to filter files. Only files whose
|
||||
path starts with this prefix will be included. Example:
|
||||
"src/google/adk/" to only include ADK source files.
|
||||
|
||||
Returns:
|
||||
A dictionary containing the status and a summary of changed files.
|
||||
"""
|
||||
# Use local git if valid path is provided (avoids GitHub API 300-file limit)
|
||||
if local_repo_path and os.path.isdir(os.path.join(local_repo_path, ".git")):
|
||||
return _get_changed_files_from_local_git(
|
||||
local_repo_path, start_tag, end_tag, repo_owner, repo_name, path_filter
|
||||
)
|
||||
|
||||
# Fall back to GitHub API (limited to 300 files)
|
||||
url = f"{GITHUB_BASE_URL}/repos/{repo_owner}/{repo_name}/compare/{start_tag}...{end_tag}"
|
||||
|
||||
try:
|
||||
@@ -654,8 +672,146 @@ def get_changed_files_summary(
|
||||
f"https://github.com/{repo_owner}/{repo_name}"
|
||||
f"/compare/{start_tag}...{end_tag}"
|
||||
),
|
||||
"note": (
|
||||
(
|
||||
"Using GitHub API which is limited to 300 files. "
|
||||
"Provide local_repo_path to get complete file list."
|
||||
)
|
||||
if len(formatted_files) >= 300
|
||||
else None
|
||||
),
|
||||
}
|
||||
except requests.exceptions.HTTPError as e:
|
||||
return error_response(f"HTTP Error: {e}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
return error_response(f"Request Error: {e}")
|
||||
|
||||
|
||||
def _get_changed_files_from_local_git(
|
||||
local_repo_path: str,
|
||||
start_tag: str,
|
||||
end_tag: str,
|
||||
repo_owner: str,
|
||||
repo_name: str,
|
||||
path_filter: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Gets changed files using local git commands (no file limit).
|
||||
|
||||
Args:
|
||||
local_repo_path: Path to local git repository.
|
||||
start_tag: The older tag (base) for the comparison.
|
||||
end_tag: The newer tag (head) for the comparison.
|
||||
repo_owner: Repository owner for compare URL.
|
||||
repo_name: Repository name for compare URL.
|
||||
path_filter: Optional path prefix to filter files.
|
||||
|
||||
Returns:
|
||||
A dictionary containing the status and a summary of changed files.
|
||||
"""
|
||||
try:
|
||||
# Fetch tags to ensure we have them
|
||||
subprocess.run(
|
||||
["git", "fetch", "--tags"],
|
||||
cwd=local_repo_path,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
# Get list of changed files with their status
|
||||
diff_result = subprocess.run(
|
||||
["git", "diff", "--name-status", f"{start_tag}...{end_tag}"],
|
||||
cwd=local_repo_path,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
|
||||
# Get numstat for additions/deletions
|
||||
numstat_result = subprocess.run(
|
||||
["git", "diff", "--numstat", f"{start_tag}...{end_tag}"],
|
||||
cwd=local_repo_path,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
|
||||
# Parse numstat output (additions, deletions, filename)
|
||||
file_stats: Dict[str, Dict[str, int]] = {}
|
||||
for line in numstat_result.stdout.strip().split("\n"):
|
||||
if not line:
|
||||
continue
|
||||
parts = line.split("\t")
|
||||
if len(parts) >= 3:
|
||||
additions = int(parts[0]) if parts[0] != "-" else 0
|
||||
deletions = int(parts[1]) if parts[1] != "-" else 0
|
||||
filename = parts[2]
|
||||
file_stats[filename] = {
|
||||
"additions": additions,
|
||||
"deletions": deletions,
|
||||
"changes": additions + deletions,
|
||||
}
|
||||
|
||||
# Parse name-status output and combine with numstat
|
||||
status_map = {
|
||||
"A": "added",
|
||||
"D": "removed",
|
||||
"M": "modified",
|
||||
"R": "renamed",
|
||||
"C": "copied",
|
||||
}
|
||||
|
||||
files_by_dir: Dict[str, List[Dict[str, Any]]] = {}
|
||||
formatted_files = []
|
||||
|
||||
for line in diff_result.stdout.strip().split("\n"):
|
||||
if not line:
|
||||
continue
|
||||
parts = line.split("\t")
|
||||
if len(parts) >= 2:
|
||||
status_code = parts[0][0] # First char is the status
|
||||
filename = parts[-1] # Last part is filename (handles renames)
|
||||
|
||||
# Apply path filter if specified
|
||||
if path_filter and not filename.startswith(path_filter):
|
||||
continue
|
||||
|
||||
stats = file_stats.get(
|
||||
filename,
|
||||
{
|
||||
"additions": 0,
|
||||
"deletions": 0,
|
||||
"changes": 0,
|
||||
},
|
||||
)
|
||||
|
||||
file_info = {
|
||||
"relative_path": filename,
|
||||
"status": status_map.get(status_code, "modified"),
|
||||
"additions": stats["additions"],
|
||||
"deletions": stats["deletions"],
|
||||
"changes": stats["changes"],
|
||||
}
|
||||
formatted_files.append(file_info)
|
||||
|
||||
# Group by top-level directory
|
||||
dir_parts = filename.split("/")
|
||||
top_dir = dir_parts[0] if dir_parts else "root"
|
||||
if top_dir not in files_by_dir:
|
||||
files_by_dir[top_dir] = []
|
||||
files_by_dir[top_dir].append(file_info)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"total_files": len(formatted_files),
|
||||
"files": formatted_files,
|
||||
"files_by_directory": files_by_dir,
|
||||
"compare_url": (
|
||||
f"https://github.com/{repo_owner}/{repo_name}"
|
||||
f"/compare/{start_tag}...{end_tag}"
|
||||
),
|
||||
}
|
||||
except subprocess.CalledProcessError as e:
|
||||
return error_response(f"Git command failed: {e.stderr}")
|
||||
except (OSError, ValueError) as e:
|
||||
return error_response(f"Error getting changed files: {e}")
|
||||
|
||||
@@ -96,7 +96,7 @@ def add_comment_to_issue(issue_number: int, comment: str) -> dict[str, any]:
|
||||
comment: comment to add
|
||||
|
||||
Returns:
|
||||
The the status of this request, with the applied comment when successful.
|
||||
The status of this request, with the applied comment when successful.
|
||||
"""
|
||||
print(f"Attempting to add comment '{comment}' to issue #{issue_number}")
|
||||
url = f"{GITHUB_BASE_URL}/repos/{OWNER}/{REPO}/issues/{issue_number}/comments"
|
||||
@@ -119,7 +119,7 @@ def list_comments_on_issue(issue_number: int) -> dict[str, any]:
|
||||
issue_number: issue number of the GitHub issue
|
||||
|
||||
Returns:
|
||||
The the status of this request, with the list of comments when successful.
|
||||
The status of this request, with the list of comments when successful.
|
||||
"""
|
||||
print(f"Attempting to list comments on issue #{issue_number}")
|
||||
url = f"{GITHUB_BASE_URL}/repos/{OWNER}/{REPO}/issues/{issue_number}/comments"
|
||||
|
||||
@@ -166,7 +166,7 @@ def add_label_to_pr(pr_number: int, label: str) -> dict[str, Any]:
|
||||
label: the label to add
|
||||
|
||||
Returns:
|
||||
The the status of this request, with the applied label and response when
|
||||
The status of this request, with the applied label and response when
|
||||
successful.
|
||||
"""
|
||||
print(f"Attempting to add label '{label}' to PR #{pr_number}")
|
||||
@@ -201,7 +201,7 @@ def add_comment_to_pr(pr_number: int, comment: str) -> dict[str, Any]:
|
||||
comment: the comment to add
|
||||
|
||||
Returns:
|
||||
The the status of this request, with the applied comment when successful.
|
||||
The status of this request, with the applied comment when successful.
|
||||
"""
|
||||
print(f"Attempting to add comment '{comment}' to issue #{pr_number}")
|
||||
|
||||
|
||||
@@ -230,7 +230,7 @@ def change_issue_type(issue_number: int, issue_type: str) -> dict[str, Any]:
|
||||
issue_type: issue type to assign
|
||||
|
||||
Returns:
|
||||
The the status of this request, with the applied issue type when successful.
|
||||
The status of this request, with the applied issue type when successful.
|
||||
"""
|
||||
print(
|
||||
f"Attempting to change issue type '{issue_type}' to issue #{issue_number}"
|
||||
|
||||
@@ -37,10 +37,6 @@ questions in the same session, and the agent will maintain context.
|
||||
* `get_data_agent_info`: Retrieves details about a specific Data Agent given
|
||||
its full resource name.
|
||||
* `ask_data_agent`: Chats with a specific Data Agent using natural language.
|
||||
This tool maintains conversation state: if you ask multiple
|
||||
questions to the same agent in one session, it will use the same
|
||||
conversation, allowing for follow-ups. If you switch agents, a new
|
||||
conversation will be started for the new agent.
|
||||
|
||||
## How to Run
|
||||
|
||||
|
||||
@@ -29,6 +29,11 @@ def concat_number_and_string(num: int, s: str) -> str:
|
||||
return str(num) + ': ' + s
|
||||
|
||||
|
||||
def write_document(document: str) -> dict[str, str]:
|
||||
"""Write a document."""
|
||||
return {'status': 'ok'}
|
||||
|
||||
|
||||
root_agent = Agent(
|
||||
model='gemini-3-pro-preview',
|
||||
name='hello_world_stream_fc_args',
|
||||
@@ -38,9 +43,14 @@ root_agent = Agent(
|
||||
You can use the `concat_number_and_string` tool to concatenate a number and a string.
|
||||
You should always call the concat_number_and_string tool to concatenate a number and a string.
|
||||
You should never concatenate on your own.
|
||||
|
||||
You can use the `write_document` tool to write a document.
|
||||
You should always call the write_document tool to write a document.
|
||||
You should never write a document on your own.
|
||||
""",
|
||||
tools=[
|
||||
concat_number_and_string,
|
||||
write_document,
|
||||
],
|
||||
generate_content_config=types.GenerateContentConfig(
|
||||
automatic_function_calling=types.AutomaticFunctionCallingConfig(
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# MCP Toolset OAuth Authentication Sample
|
||||
|
||||
This sample demonstrates the toolset authentication feature where OAuth credentials are required for both tool listing and tool calling.
|
||||
|
||||
## Overview
|
||||
|
||||
The toolset authentication flow works in two phases:
|
||||
|
||||
1. **Phase 1**: When the agent tries to get tools from the MCP server without credentials, the toolset signals "authentication required" and returns an auth request event.
|
||||
|
||||
2. **Phase 2**: After the user provides OAuth credentials, the agent can successfully list and call tools.
|
||||
|
||||
## Files
|
||||
|
||||
- `oauth_mcp_server.py` - MCP server that requires Bearer token authentication
|
||||
- `agent.py` - Agent configuration with OAuth-protected MCP toolset
|
||||
- `main.py` - Test script demonstrating the two-phase auth flow
|
||||
|
||||
## Running the Sample
|
||||
|
||||
1. Start the MCP server in one terminal:
|
||||
|
||||
```bash
|
||||
PYTHONPATH=src python contributing/samples/mcp_toolset_auth/oauth_mcp_server.py
|
||||
```
|
||||
|
||||
2. Run the test script in another terminal:
|
||||
|
||||
```bash
|
||||
PYTHONPATH=src python contributing/samples/mcp_toolset_auth/main.py
|
||||
```
|
||||
|
||||
## Expected Behavior
|
||||
|
||||
1. First invocation yields an `adk_request_credential` function call
|
||||
2. The credential ID is `_adk_toolset_auth_McpToolset` to indicate toolset auth
|
||||
3. After providing the access token, the agent can list and call tools
|
||||
|
||||
## Testing with ADK Web UI
|
||||
|
||||
You can also test with the ADK web UI:
|
||||
|
||||
```bash
|
||||
adk web contributing/samples/mcp_toolset_auth
|
||||
```
|
||||
|
||||
Note: The web UI will display the auth request and you'll need to manually provide credentials.
|
||||
@@ -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,76 @@
|
||||
# Copyright 2025 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Agent that uses MCP toolset requiring OAuth authentication.
|
||||
|
||||
This agent demonstrates the toolset authentication feature where OAuth
|
||||
credentials are required for both tool listing and tool calling.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi.openapi.models import OAuth2
|
||||
from fastapi.openapi.models import OAuthFlowAuthorizationCode
|
||||
from fastapi.openapi.models import OAuthFlows
|
||||
from google.adk.agents import LlmAgent
|
||||
from google.adk.auth.auth_credential import AuthCredential
|
||||
from google.adk.auth.auth_credential import AuthCredentialTypes
|
||||
from google.adk.auth.auth_credential import OAuth2Auth
|
||||
from google.adk.tools.mcp_tool.mcp_session_manager import StreamableHTTPConnectionParams
|
||||
from google.adk.tools.mcp_tool.mcp_toolset import McpToolset
|
||||
|
||||
# OAuth2 auth scheme with authorization code flow
|
||||
# This specifies the OAuth metadata needed for the full OAuth flow
|
||||
auth_scheme = OAuth2(
|
||||
flows=OAuthFlows(
|
||||
authorizationCode=OAuthFlowAuthorizationCode(
|
||||
authorizationUrl='https://example.com/oauth/authorize',
|
||||
tokenUrl='https://example.com/oauth/token',
|
||||
scopes={'read': 'Read access', 'write': 'Write access'},
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# OAuth credential with client credentials (used for token exchange)
|
||||
# In a real scenario, this would be used to obtain the access token
|
||||
auth_credential = AuthCredential(
|
||||
auth_type=AuthCredentialTypes.OAUTH2,
|
||||
oauth2=OAuth2Auth(
|
||||
client_id='test_client_id',
|
||||
client_secret='test_client_secret',
|
||||
),
|
||||
)
|
||||
|
||||
# Create the MCP toolset with OAuth authentication
|
||||
mcp_toolset = McpToolset(
|
||||
connection_params=StreamableHTTPConnectionParams(
|
||||
url='http://localhost:3001/mcp',
|
||||
),
|
||||
auth_scheme=auth_scheme,
|
||||
auth_credential=auth_credential,
|
||||
)
|
||||
|
||||
# Define the agent that uses the OAuth-protected MCP toolset
|
||||
root_agent = LlmAgent(
|
||||
model='gemini-2.0-flash',
|
||||
name='oauth_mcp_agent',
|
||||
instruction="""You are a helpful assistant that can access user information.
|
||||
|
||||
You have access to tools that require authentication:
|
||||
- get_user_profile: Get profile information for a specific user
|
||||
- list_users: List all available users
|
||||
|
||||
When the user asks about users, use these tools to help them.""",
|
||||
tools=[mcp_toolset],
|
||||
)
|
||||
@@ -0,0 +1,168 @@
|
||||
# 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.
|
||||
|
||||
"""Test script for MCP Toolset OAuth Authentication Flow.
|
||||
|
||||
This script demonstrates the two-phase tool discovery flow:
|
||||
1. First invocation: Agent tries to get tools, auth is required, returns auth
|
||||
request event (adk_request_credential)
|
||||
2. User provides OAuth credentials (simulated)
|
||||
3. Second invocation: Agent has credentials, can list and call tools
|
||||
|
||||
Usage:
|
||||
# Start the MCP server first (in another terminal):
|
||||
PYTHONPATH=src python contributing/samples/mcp_toolset_auth/oauth_mcp_server.py
|
||||
|
||||
# Run the demo:
|
||||
PYTHONPATH=src python contributing/samples/mcp_toolset_auth/main.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from agent import auth_credential
|
||||
from agent import auth_scheme
|
||||
from agent import mcp_toolset
|
||||
from agent import root_agent
|
||||
from google.adk.auth.auth_credential import AuthCredential
|
||||
from google.adk.auth.auth_credential import AuthCredentialTypes
|
||||
from google.adk.auth.auth_credential import OAuth2Auth
|
||||
from google.adk.auth.auth_tool import AuthConfig
|
||||
from google.adk.runners import Runner
|
||||
from google.adk.sessions.in_memory_session_service import InMemorySessionService
|
||||
from google.genai import types
|
||||
|
||||
|
||||
async def run_demo():
|
||||
"""Run demo with real MCP server."""
|
||||
print('=' * 60)
|
||||
print('MCP Toolset OAuth Authentication Demo')
|
||||
print('=' * 60)
|
||||
print('\nNote: Make sure the MCP server is running:')
|
||||
print(' python oauth_mcp_server.py\n')
|
||||
|
||||
# Create session service and runner
|
||||
session_service = InMemorySessionService()
|
||||
runner = Runner(
|
||||
agent=root_agent,
|
||||
app_name='toolset_auth_demo',
|
||||
session_service=session_service,
|
||||
)
|
||||
|
||||
# Create a session
|
||||
session = await session_service.create_session(
|
||||
app_name='toolset_auth_demo',
|
||||
user_id='test_user',
|
||||
)
|
||||
|
||||
print(f'Session created: {session.id}')
|
||||
print('\n--- Phase 1: Initial request (no credentials) ---\n')
|
||||
|
||||
# First invocation - should trigger auth request
|
||||
user_message = 'List all users'
|
||||
print(f'User: {user_message}')
|
||||
|
||||
events = []
|
||||
auth_function_call_id = None
|
||||
max_events = 10
|
||||
|
||||
try:
|
||||
async for event in runner.run_async(
|
||||
session_id=session.id,
|
||||
user_id='test_user',
|
||||
new_message=types.Content(
|
||||
role='user',
|
||||
parts=[types.Part(text=user_message)],
|
||||
),
|
||||
):
|
||||
events.append(event)
|
||||
print(f'\nEvent from {event.author}:')
|
||||
if event.content and event.content.parts:
|
||||
for part in event.content.parts:
|
||||
if part.text:
|
||||
print(f' Text: {part.text}')
|
||||
if part.function_call:
|
||||
print(f' Function call: {part.function_call.name}')
|
||||
if part.function_call.name == 'adk_request_credential':
|
||||
auth_function_call_id = part.function_call.id
|
||||
|
||||
if len(events) >= max_events:
|
||||
print(f'\n** SAFETY LIMIT ({max_events} events) **')
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
print(f'\nError: {e}')
|
||||
print('Make sure the MCP server is running!')
|
||||
await mcp_toolset.close()
|
||||
return
|
||||
|
||||
if auth_function_call_id:
|
||||
print('\n** Auth request detected! **')
|
||||
print('\n--- Phase 2: Provide OAuth credentials ---\n')
|
||||
|
||||
# Simulate user providing OAuth credentials after completing OAuth flow
|
||||
auth_response = AuthConfig(
|
||||
auth_scheme=auth_scheme,
|
||||
raw_auth_credential=auth_credential,
|
||||
exchanged_auth_credential=AuthCredential(
|
||||
auth_type=AuthCredentialTypes.OAUTH2,
|
||||
oauth2=OAuth2Auth(
|
||||
access_token='test_access_token_12345',
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
print('Providing access token: test_access_token_12345')
|
||||
|
||||
auth_response_message = types.Content(
|
||||
role='user',
|
||||
parts=[
|
||||
types.Part(
|
||||
function_response=types.FunctionResponse(
|
||||
name='adk_request_credential',
|
||||
id=auth_function_call_id,
|
||||
response=auth_response.model_dump(exclude_none=True),
|
||||
)
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
async for event in runner.run_async(
|
||||
session_id=session.id,
|
||||
user_id='test_user',
|
||||
new_message=auth_response_message,
|
||||
):
|
||||
print(f'\nEvent from {event.author}:')
|
||||
if event.content and event.content.parts:
|
||||
for part in event.content.parts:
|
||||
if part.text:
|
||||
text = (
|
||||
part.text[:200] + '...' if len(part.text) > 200 else part.text
|
||||
)
|
||||
print(f' Text: {text}')
|
||||
if part.function_call:
|
||||
print(f' Function call: {part.function_call.name}')
|
||||
else:
|
||||
print('\n** No auth request - credentials may already be available **')
|
||||
|
||||
print('\n' + '=' * 60)
|
||||
print('Demo completed')
|
||||
print('=' * 60)
|
||||
|
||||
await mcp_toolset.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(run_demo())
|
||||
@@ -0,0 +1,120 @@
|
||||
# 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.
|
||||
|
||||
"""MCP Server that requires OAuth Bearer token for both tool listing and calling.
|
||||
|
||||
This server validates the Authorization header on every request including:
|
||||
- Tool listing (list_tools endpoint)
|
||||
- Tool calling (call_tool endpoint)
|
||||
|
||||
This is used to test the toolset authentication feature in ADK.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi import HTTPException
|
||||
from fastapi import Request
|
||||
from mcp.server.fastmcp import Context
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
import uvicorn
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Expected OAuth token for testing
|
||||
VALID_TOKEN = 'test_access_token_12345'
|
||||
|
||||
# Create FastMCP server
|
||||
mcp = FastMCP('OAuth Protected MCP Server', host='localhost', port=3001)
|
||||
|
||||
|
||||
def validate_auth_header(request: Request) -> bool:
|
||||
"""Validate the Authorization header contains a valid Bearer token."""
|
||||
auth_header = request.headers.get('authorization', '')
|
||||
if not auth_header.startswith('Bearer '):
|
||||
logger.warning('Missing or invalid Authorization header: %s', auth_header)
|
||||
return False
|
||||
|
||||
token = auth_header[7:] # Remove 'Bearer ' prefix
|
||||
if token != VALID_TOKEN:
|
||||
logger.warning('Invalid token: %s', token)
|
||||
return False
|
||||
|
||||
logger.info('Valid token received')
|
||||
return True
|
||||
|
||||
|
||||
@mcp.tool(description='Get user profile information. Requires authentication.')
|
||||
def get_user_profile(user_id: str, context: Context) -> dict:
|
||||
"""Return user profile data for the given user ID."""
|
||||
logger.info('get_user_profile called for user: %s', user_id)
|
||||
|
||||
if context.request_context and context.request_context.request:
|
||||
if not validate_auth_header(context.request_context.request):
|
||||
return {'error': 'Unauthorized - invalid or missing token'}
|
||||
|
||||
# Mock user data
|
||||
users = {
|
||||
'user1': {'id': 'user1', 'name': 'Alice', 'email': 'alice@example.com'},
|
||||
'user2': {'id': 'user2', 'name': 'Bob', 'email': 'bob@example.com'},
|
||||
}
|
||||
|
||||
if user_id in users:
|
||||
return users[user_id]
|
||||
return {'error': f'User {user_id} not found'}
|
||||
|
||||
|
||||
@mcp.tool(description='List all available users. Requires authentication.')
|
||||
def list_users(context: Context) -> dict:
|
||||
"""Return a list of all users."""
|
||||
logger.info('list_users called')
|
||||
|
||||
if context.request_context and context.request_context.request:
|
||||
if not validate_auth_header(context.request_context.request):
|
||||
return {'error': 'Unauthorized - invalid or missing token'}
|
||||
|
||||
return {
|
||||
'users': [
|
||||
{'id': 'user1', 'name': 'Alice'},
|
||||
{'id': 'user2', 'name': 'Bob'},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
# Create custom FastAPI app to add auth middleware for list_tools
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.middleware('http')
|
||||
async def auth_middleware(request: Request, call_next):
|
||||
"""Middleware to validate auth on all MCP endpoints."""
|
||||
# Check if this is an MCP request
|
||||
if request.url.path.startswith('/mcp'):
|
||||
if not validate_auth_header(request):
|
||||
raise HTTPException(status_code=401, detail='Unauthorized')
|
||||
return await call_next(request)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print(f'Starting OAuth Protected MCP server on http://localhost:3001')
|
||||
print(f'Expected token: Bearer {VALID_TOKEN}')
|
||||
print(
|
||||
'This server requires authentication for both tool listing and calling.'
|
||||
)
|
||||
|
||||
# Run with streamable-http transport
|
||||
mcp.run(transport='streamable-http')
|
||||
+1
-1
@@ -101,7 +101,7 @@ root_agent = Agent(
|
||||
|
||||
### Define a multi-agent system:
|
||||
|
||||
Define a multi-agent system with coordinator agent, greeter agent, and task execution agent. Then ADK engine and the model will guide the agents works together to accomplish the task.
|
||||
Define a multi-agent system with coordinator agent, greeter agent, and task execution agent. Then ADK engine and the model will guide the agents to work together to accomplish the task.
|
||||
|
||||
```python
|
||||
from google.adk.agents import LlmAgent, BaseAgent
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user