Compare commits

..

28 Commits

Author SHA1 Message Date
AlexTalreja
be31376212 feat: LLM testing playground for UI 2025-08-05 00:36:36 -07:00
Alex Talreja
db8473263e feat: add login with google button for automatic id token retrieval 2025-07-28 21:22:07 +00:00
Alex Talreja
2680864dca feat: search and inspect toolsets 2025-07-24 21:14:36 +00:00
Alex Talreja
010037af19 refactor: export function to loadTools into navbar 2025-07-24 18:18:25 +00:00
Alex Talreja
f69ec70eaf feat: add token instructions to header modal 2025-07-23 21:06:22 +00:00
Alex Talreja
187fe69a8b feat: allow users to edit headers when running tools 2025-07-23 19:13:29 +00:00
Alex Talreja
45de436118 resolve comments 2025-07-22 21:49:38 +00:00
Alex Talreja
1598e32e34 fix handling of boolean params 2025-07-21 21:26:46 +00:00
Alex Talreja
ae68aa58bd feat: invoke tools from Toolbox UI 2025-07-21 21:26:45 +00:00
AlexTalreja
7fa8633a20 test(internal/server): update web tests to verify resource paths (#934)
Update `web_test` to verify linked resources such as the `style.css`
file and `.js` components are accessible.

Also added test cases to check URLs with and without a trailing slash
(ex: `/ui` and `/ui/`).
2025-07-21 14:21:38 -07:00
AlexTalreja
615e6e76d9 feat: inspect tools from Toolbox UI (#887)
Add tool inspect functionality to Toolbox UI. 

Selecting the tools tab will open popup for specific tools, and
selecting a specific tools populates name, description, and parameters.
2025-07-21 10:49:38 -07:00
AlexTalreja
9624d845f2 feat: launch web server with --ui flag (#780)
Add a flag `--ui` which will launch the Toolbox UI web server and
a skeleton HTML page.

The web server is on the same port as Toolbox and will serve HTML pages
from `static/`
2025-07-15 14:12:27 -07:00
Anubhav Dhawan
72a7282797 docs: Add Toolbox SDKs repo links to relevant doc snippets (#828)
This PR adds Toolbox SDK github repo links to the relevant parts where
these SDKs are introduced in the `README` for additional context.
2025-07-09 16:34:57 +05:30
Anmol Shukla
29fe3b93cd docs: fix copy to clipboard button visibility in light mode (#826)
This PR fixes the issue #791 and updated the info box color so that tags
are visible in dark mode as well in docsite.
2025-07-09 14:13:12 +05:30
Anubhav Dhawan
fb3f66acf4 docs: Correct link for Cloud Run datasource setup (#794)
Updated the link in the Cloud Run deployment guide for `tools.yaml`
setup. The previous link incorrectly pointed to a `localhost` source
example, which causes confusion and deployment failures. The new link
directs users to the guide for configuring cloud-based sources, ensuring
a correct setup.
2025-07-09 06:11:18 +00:00
Yuan Teoh
1f95eb134b test: add more time to spanner integration test ctx (#819)
Occasionally the Spanner integration test's `context` timeout before the
`DROP` operation could finish.
2025-07-09 01:21:22 +00:00
AlexTalreja
4c240ac3c9 feat: dynamic reloading for toolbox config (#800)
Allow Toolbox server to automatically update when users modify their
tool configuration file(s), instead of requiring a restart.

This feature is automatically enabled, but can be turned off with the
flag `--disable-reload`.
2025-07-08 17:28:12 -07:00
Huan Chen
c6ab74c5da feat: add optional projectID parameter to bigquery tools (#799)
Optional projectID parameter enables dynamic, cross-project resource
access in BigQuery tools.

This allows a single tool configuration to target different projects at
runtime, rather than being fixed to the project in its source
configuration.

---------

Co-authored-by: Yuan Teoh <45984206+Yuan325@users.noreply.github.com>
Co-authored-by: Wenxin Du <117315983+duwenxin99@users.noreply.github.com>
2025-07-08 18:02:42 -04:00
Yuan Teoh
04e2529ba9 test: add null column test case (#768)
Add integration tests to check for `null` columns. ref #757
2025-07-08 20:20:16 +00:00
Mend Renovate
53dd247e6e chore(deps): update module google.golang.org/api to v0.240.0 (#778)
This PR contains the following updates:

| Package | Change | Age | Confidence |
|---|---|---|---|
|
[google.golang.org/api](https://redirect.github.com/googleapis/google-api-go-client)
| `v0.239.0` -> `v0.240.0` |
[![age](https://developer.mend.io/api/mc/badges/age/go/google.golang.org%2fapi/v0.240.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/go/google.golang.org%2fapi/v0.239.0/v0.240.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>googleapis/google-api-go-client
(google.golang.org/api)</summary>

###
[`v0.240.0`](https://redirect.github.com/googleapis/google-api-go-client/releases/tag/v0.240.0)

[Compare
Source](https://redirect.github.com/googleapis/google-api-go-client/compare/v0.239.0...v0.240.0)

##### Features

- **all:** Auto-regenerate discovery clients
([#&#8203;3210](https://redirect.github.com/googleapis/google-api-go-client/issues/3210))
([c0efdb5](c0efdb50d5))
- **all:** Auto-regenerate discovery clients
([#&#8203;3212](https://redirect.github.com/googleapis/google-api-go-client/issues/3212))
([c699558](c699558a9c))
- **all:** Auto-regenerate discovery clients
([#&#8203;3214](https://redirect.github.com/googleapis/google-api-go-client/issues/3214))
([7b43598](7b43598833))
- **all:** Auto-regenerate discovery clients
([#&#8203;3215](https://redirect.github.com/googleapis/google-api-go-client/issues/3215))
([22e2c38](22e2c38068))
- **all:** Auto-regenerate discovery clients
([#&#8203;3216](https://redirect.github.com/googleapis/google-api-go-client/issues/3216))
([e8c3504](e8c3504399))
- **all:** Auto-regenerate discovery clients
([#&#8203;3217](https://redirect.github.com/googleapis/google-api-go-client/issues/3217))
([604190c](604190c29e))
- **all:** Auto-regenerate discovery clients
([#&#8203;3218](https://redirect.github.com/googleapis/google-api-go-client/issues/3218))
([0a46af7](0a46af7bb3))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/googleapis/genai-toolbox).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4xNy4yIiwidXBkYXRlZEluVmVyIjoiNDEuMTcuMiIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: Yuan <45984206+Yuan325@users.noreply.github.com>
2025-07-07 16:08:01 -07:00
Twisha Bansal
648eede62b docs: add introduction snippets for JS SDK (#785)
Using nested tabs in hugo is hard and flaky. So, we're using different
heading for the multiple languages here.
2025-07-08 00:11:49 +05:30
Yuan
9b2dfcc553 chore: update int test variable name to be consistent (#766)
Update `_` variables to camelcase.
2025-07-04 05:21:39 +00:00
Twisha Bansal
cb514209b6 docs: add JS SDK to readme (#776)
Co-authored-by: Kurtis Van Gent <31518063+kurtisvg@users.noreply.github.com>
2025-07-03 22:35:13 +05:30
Twisha Bansal
0a93b0482c fix: fix docs preview deployment pipeline (#787)
The code for the preview build is now sourced from the target branch,
whereas it was previously sourced from the main branch.
2025-07-03 21:54:10 +05:30
release-please[bot]
f13e9635ba chore(main): release 0.8.0 (#689)
🤖 I have created a release *beep* *boop*
---


##
[0.8.0](https://github.com/googleapis/genai-toolbox/compare/v0.7.0...v0.8.0)
(2025-07-02)


### ⚠ BREAKING CHANGES

* **postgres,mssql,cloudsqlmssql:** encode source connection url for
sources ([#727](https://github.com/googleapis/genai-toolbox/issues/727))

### Features

* Add support for multiple YAML configuration files
([#760](https://github.com/googleapis/genai-toolbox/issues/760))
([40679d7](40679d700e))
* Add support for optional parameters
([#617](https://github.com/googleapis/genai-toolbox/issues/617))
([4827771](4827771b78)),
closes [#475](https://github.com/googleapis/genai-toolbox/issues/475)
* **mcp:** Support MCP version 2025-03-26
([#755](https://github.com/googleapis/genai-toolbox/issues/755))
([474df57](474df57d62))
* **sources/http:** Support disable SSL verification for HTTP Source
([#674](https://github.com/googleapis/genai-toolbox/issues/674))
([4055b0c](4055b0c356))
* **tools/bigquery:** Add templateParameters field for bigquery
([#699](https://github.com/googleapis/genai-toolbox/issues/699))
([f5f771b](f5f771b0f3))
* **tools/bigtable:** Add templateParameters field for bigtable
([#692](https://github.com/googleapis/genai-toolbox/issues/692))
([1c06771](1c067715fa))
* **tools/couchbase:** Add templateParameters field for couchbase
([#723](https://github.com/googleapis/genai-toolbox/issues/723))
([9197186](9197186b8b))
* **tools/http:** Add support for HTTP Tool pathParams
([#726](https://github.com/googleapis/genai-toolbox/issues/726))
([fd300dc](fd300dc606))
* **tools/redis:** Add Redis Source and Tool
([#519](https://github.com/googleapis/genai-toolbox/issues/519))
([f0aef29](f0aef29b0c))
* **tools/spanner:** Add templateParameters field for spanner
([#691](https://github.com/googleapis/genai-toolbox/issues/691))
([075dfa4](075dfa47e1))
* **tools/sqlitesql:** Add templateParameters field for sqlitesql
([#687](https://github.com/googleapis/genai-toolbox/issues/687))
([75e254c](75e254c0a4))
* **tools/valkey:** Add Valkey Source and Tool
([#532](https://github.com/googleapis/genai-toolbox/issues/532))
([054ec19](054ec198b9))


### Bug Fixes

* **bigquery,mssql:** Fix panic on tools with array param
([#722](https://github.com/googleapis/genai-toolbox/issues/722))
([7a6644c](7a6644cf0c))
* **postgres,mssql,cloudsqlmssql:** Encode source connection url for
sources ([#727](https://github.com/googleapis/genai-toolbox/issues/727))
([67964d9](67964d939f)),
closes [#717](https://github.com/googleapis/genai-toolbox/issues/717)
* Set default value to field's type during unmarshalling
([#774](https://github.com/googleapis/genai-toolbox/issues/774))
([fafed24](fafed24858)),
closes [#771](https://github.com/googleapis/genai-toolbox/issues/771)
* **server/mcp:** Do not listen from port for stdio
([#719](https://github.com/googleapis/genai-toolbox/issues/719))
([d51dbc7](d51dbc759b)),
closes [#711](https://github.com/googleapis/genai-toolbox/issues/711)
* **tools/mysqlexecutesql:** Handle nil panic and connection leak in
Invoke ([#757](https://github.com/googleapis/genai-toolbox/issues/757))
([7badba4](7badba42ee))
* **tools/mysqlsql:** Handle nil panic and connection leak in invoke
([#758](https://github.com/googleapis/genai-toolbox/issues/758))
([cbb4a33](cbb4a33351))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

---------

Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com>
Co-authored-by: Yuan <45984206+Yuan325@users.noreply.github.com>
2025-07-02 09:30:33 -06:00
Yuan
fafed24858 fix: set default value to field's type during unmarshalling (#774)
When go-yaml decode into CommonParameter with Default being an any type,
int will be converted into []uint64.
It will fail the Parse() when the value is being used since it does not
belong to either of the int types.

Unmarshal `default` value into each field's type directly.

Fixes #771
2025-07-02 14:58:42 +00:00
Mend Renovate
6337434623 chore(deps): update module github.com/go-playground/validator/v10 to v10.27.0 (#775)
This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
|
[github.com/go-playground/validator/v10](https://redirect.github.com/go-playground/validator)
| `v10.26.0` -> `v10.27.0` |
[![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fgo-playground%2fvalidator%2fv10/v10.27.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/go/github.com%2fgo-playground%2fvalidator%2fv10/v10.27.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/go/github.com%2fgo-playground%2fvalidator%2fv10/v10.26.0/v10.27.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fgo-playground%2fvalidator%2fv10/v10.26.0/v10.27.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>go-playground/validator
(github.com/go-playground/validator/v10)</summary>

###
[`v10.27.0`](https://redirect.github.com/go-playground/validator/releases/tag/v10.27.0):
Release 10.27.0

[Compare
Source](https://redirect.github.com/go-playground/validator/compare/v10.26.0...v10.27.0)

#### What's Changed

- Fix Release version badge on README page by
[@&#8203;nodivbyzero](https://redirect.github.com/nodivbyzero) in
[https://github.com/go-playground/validator/pull/1406](https://redirect.github.com/go-playground/validator/pull/1406)
- fix russian E.164 error message by
[@&#8203;prigornitskiy](https://redirect.github.com/prigornitskiy) in
[https://github.com/go-playground/validator/pull/1349](https://redirect.github.com/go-playground/validator/pull/1349)
- chore: remove unnecessary statement by
[@&#8203;qshuai](https://redirect.github.com/qshuai) in
[https://github.com/go-playground/validator/pull/1200](https://redirect.github.com/go-playground/validator/pull/1200)
- Re-enable several linters by
[@&#8203;nodivbyzero](https://redirect.github.com/nodivbyzero) in
[https://github.com/go-playground/validator/pull/1412](https://redirect.github.com/go-playground/validator/pull/1412)
- add support to tag validateFn by
[@&#8203;peczenyj](https://redirect.github.com/peczenyj) in
[https://github.com/go-playground/validator/pull/1363](https://redirect.github.com/go-playground/validator/pull/1363)
- Bump golang.org/x/crypto from 0.33.0 to 0.35.0 in
/\_examples/validate\_fn by
[@&#8203;dependabot](https://redirect.github.com/dependabot) in
[https://github.com/go-playground/validator/pull/1418](https://redirect.github.com/go-playground/validator/pull/1418)
- Bump golang.org/x/net from 0.34.0 to 0.38.0 in
/\_examples/validate\_fn by
[@&#8203;dependabot](https://redirect.github.com/dependabot) in
[https://github.com/go-playground/validator/pull/1419](https://redirect.github.com/go-playground/validator/pull/1419)
- Align required\_without with the contract stated in the documentation
by [@&#8203;jmfrees](https://redirect.github.com/jmfrees) in
[https://github.com/go-playground/validator/pull/1422](https://redirect.github.com/go-playground/validator/pull/1422)
- Add translation example by
[@&#8203;cxlblm](https://redirect.github.com/cxlblm) in
[https://github.com/go-playground/validator/pull/1394](https://redirect.github.com/go-playground/validator/pull/1394)
- doc(errors): mention RegisterTagNameFunc for FieldError.Field by
[@&#8203;khan-ajamal](https://redirect.github.com/khan-ajamal) in
[https://github.com/go-playground/validator/pull/1358](https://redirect.github.com/go-playground/validator/pull/1358)
- Bump golangci/golangci-lint-action from 7 to 8 by
[@&#8203;dependabot](https://redirect.github.com/dependabot) in
[https://github.com/go-playground/validator/pull/1425](https://redirect.github.com/go-playground/validator/pull/1425)
- feat(translation): add en translation for urn\_rfc2141 by
[@&#8203;ryanmalesic](https://redirect.github.com/ryanmalesic) in
[https://github.com/go-playground/validator/pull/1431](https://redirect.github.com/go-playground/validator/pull/1431)
- fix: panics when private field is validated by
[@&#8203;ykalchevskiy](https://redirect.github.com/ykalchevskiy) in
[https://github.com/go-playground/validator/pull/1423](https://redirect.github.com/go-playground/validator/pull/1423)
- Fix: support validation for map values with struct types by
[@&#8203;JunaidIslam2105](https://redirect.github.com/JunaidIslam2105)
in
[https://github.com/go-playground/validator/pull/1433](https://redirect.github.com/go-playground/validator/pull/1433)
- Omitzero does not work with slice and map bug by
[@&#8203;JunaidIslam2105](https://redirect.github.com/JunaidIslam2105)
in
[https://github.com/go-playground/validator/pull/1436](https://redirect.github.com/go-playground/validator/pull/1436)
- Fix: Validator panics when 'nil' is used along with required if for
slices and maps by
[@&#8203;JunaidIslam2105](https://redirect.github.com/JunaidIslam2105)
in
[https://github.com/go-playground/validator/pull/1442](https://redirect.github.com/go-playground/validator/pull/1442)
- docs: typos by [@&#8203;eqsdxr](https://redirect.github.com/eqsdxr) in
[https://github.com/go-playground/validator/pull/1440](https://redirect.github.com/go-playground/validator/pull/1440)
- fix: make "file://" fail `url` validation by
[@&#8203;bfabio](https://redirect.github.com/bfabio) in
[https://github.com/go-playground/validator/pull/1444](https://redirect.github.com/go-playground/validator/pull/1444)
- disable way too aggressive and disagreeable linters by
[@&#8203;deankarn](https://redirect.github.com/deankarn) in
[https://github.com/go-playground/validator/pull/1445](https://redirect.github.com/go-playground/validator/pull/1445)
- use golangci lint file for disables by
[@&#8203;deankarn](https://redirect.github.com/deankarn) in
[https://github.com/go-playground/validator/pull/1447](https://redirect.github.com/go-playground/validator/pull/1447)

#### New Contributors

- [@&#8203;prigornitskiy](https://redirect.github.com/prigornitskiy)
made their first contribution in
[https://github.com/go-playground/validator/pull/1349](https://redirect.github.com/go-playground/validator/pull/1349)
- [@&#8203;qshuai](https://redirect.github.com/qshuai) made their first
contribution in
[https://github.com/go-playground/validator/pull/1200](https://redirect.github.com/go-playground/validator/pull/1200)
- [@&#8203;peczenyj](https://redirect.github.com/peczenyj) made their
first contribution in
[https://github.com/go-playground/validator/pull/1363](https://redirect.github.com/go-playground/validator/pull/1363)
- [@&#8203;jmfrees](https://redirect.github.com/jmfrees) made their
first contribution in
[https://github.com/go-playground/validator/pull/1422](https://redirect.github.com/go-playground/validator/pull/1422)
- [@&#8203;cxlblm](https://redirect.github.com/cxlblm) made their first
contribution in
[https://github.com/go-playground/validator/pull/1394](https://redirect.github.com/go-playground/validator/pull/1394)
- [@&#8203;khan-ajamal](https://redirect.github.com/khan-ajamal) made
their first contribution in
[https://github.com/go-playground/validator/pull/1358](https://redirect.github.com/go-playground/validator/pull/1358)
- [@&#8203;ryanmalesic](https://redirect.github.com/ryanmalesic) made
their first contribution in
[https://github.com/go-playground/validator/pull/1431](https://redirect.github.com/go-playground/validator/pull/1431)
- [@&#8203;ykalchevskiy](https://redirect.github.com/ykalchevskiy) made
their first contribution in
[https://github.com/go-playground/validator/pull/1423](https://redirect.github.com/go-playground/validator/pull/1423)
- [@&#8203;JunaidIslam2105](https://redirect.github.com/JunaidIslam2105)
made their first contribution in
[https://github.com/go-playground/validator/pull/1433](https://redirect.github.com/go-playground/validator/pull/1433)
- [@&#8203;eqsdxr](https://redirect.github.com/eqsdxr) made their first
contribution in
[https://github.com/go-playground/validator/pull/1440](https://redirect.github.com/go-playground/validator/pull/1440)
- [@&#8203;bfabio](https://redirect.github.com/bfabio) made their first
contribution in
[https://github.com/go-playground/validator/pull/1444](https://redirect.github.com/go-playground/validator/pull/1444)

**Full Changelog**:
https://github.com/go-playground/validator/compare/v10.26.0...v10.27.0

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/googleapis/genai-toolbox).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MC42Mi4xIiwidXBkYXRlZEluVmVyIjoiNDAuNjIuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->
2025-07-02 05:32:35 +00:00
Mend Renovate
822708afaa chore(deps): update module cloud.google.com/go/bigtable to v1.38.0 (#773)
This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
|
[cloud.google.com/go/bigtable](https://redirect.github.com/googleapis/google-cloud-go)
| `v1.37.0` -> `v1.38.0` |
[![age](https://developer.mend.io/api/mc/badges/age/go/cloud.google.com%2fgo%2fbigtable/v1.38.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/go/cloud.google.com%2fgo%2fbigtable/v1.38.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/go/cloud.google.com%2fgo%2fbigtable/v1.37.0/v1.38.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/go/cloud.google.com%2fgo%2fbigtable/v1.37.0/v1.38.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/googleapis/genai-toolbox).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MC42Mi4xIiwidXBkYXRlZEluVmVyIjoiNDAuNjIuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->
2025-07-02 05:14:59 +00:00
82 changed files with 4889 additions and 923 deletions

View File

@@ -51,6 +51,7 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod
- name: Setup Hugo

View File

@@ -0,0 +1 @@
@import 'td/code-dark';

View File

@@ -48,7 +48,11 @@ enableRobotsTXT = true
pre = "<i class='fa-brands fa-github'></i>"
[markup.goldmark.renderer]
unsafe= true
unsafe= true
[markup.highlight]
noClasses = false
style = "tango"
[outputFormats]
[outputFormats.LLMS]

View File

@@ -1,5 +1,37 @@
# Changelog
## [0.8.0](https://github.com/googleapis/genai-toolbox/compare/v0.7.0...v0.8.0) (2025-07-02)
### ⚠ BREAKING CHANGES
* **postgres,mssql,cloudsqlmssql:** encode source connection url for sources ([#727](https://github.com/googleapis/genai-toolbox/issues/727))
### Features
* Add support for multiple YAML configuration files ([#760](https://github.com/googleapis/genai-toolbox/issues/760)) ([40679d7](https://github.com/googleapis/genai-toolbox/commit/40679d700eded50d19569923e2a71c51e907a8bf))
* Add support for optional parameters ([#617](https://github.com/googleapis/genai-toolbox/issues/617)) ([4827771](https://github.com/googleapis/genai-toolbox/commit/4827771b78dee9a1284a898b749509b472061527)), closes [#475](https://github.com/googleapis/genai-toolbox/issues/475)
* **mcp:** Support MCP version 2025-03-26 ([#755](https://github.com/googleapis/genai-toolbox/issues/755)) ([474df57](https://github.com/googleapis/genai-toolbox/commit/474df57d62de683079f8d12c31db53396a545fd1))
* **sources/http:** Support disable SSL verification for HTTP Source ([#674](https://github.com/googleapis/genai-toolbox/issues/674)) ([4055b0c](https://github.com/googleapis/genai-toolbox/commit/4055b0c3569c527560d7ad34262963b3dd4e282d))
* **tools/bigquery:** Add templateParameters field for bigquery ([#699](https://github.com/googleapis/genai-toolbox/issues/699)) ([f5f771b](https://github.com/googleapis/genai-toolbox/commit/f5f771b0f3d159630ff602ff55c6c66b61981446))
* **tools/bigtable:** Add templateParameters field for bigtable ([#692](https://github.com/googleapis/genai-toolbox/issues/692)) ([1c06771](https://github.com/googleapis/genai-toolbox/commit/1c067715fac06479eb0060d7067b73dba099ed92))
* **tools/couchbase:** Add templateParameters field for couchbase ([#723](https://github.com/googleapis/genai-toolbox/issues/723)) ([9197186](https://github.com/googleapis/genai-toolbox/commit/9197186b8bea1ac4ec1b39c9c5c110807c8b2ba9))
* **tools/http:** Add support for HTTP Tool pathParams ([#726](https://github.com/googleapis/genai-toolbox/issues/726)) ([fd300dc](https://github.com/googleapis/genai-toolbox/commit/fd300dc606d88bf9f7bba689e2cee4e3565537dd))
* **tools/redis:** Add Redis Source and Tool ([#519](https://github.com/googleapis/genai-toolbox/issues/519)) ([f0aef29](https://github.com/googleapis/genai-toolbox/commit/f0aef29b0c2563e2a00277fbe2784f39f16d2835))
* **tools/spanner:** Add templateParameters field for spanner ([#691](https://github.com/googleapis/genai-toolbox/issues/691)) ([075dfa4](https://github.com/googleapis/genai-toolbox/commit/075dfa47e1fd92be4847bd0aec63296146b66455))
* **tools/sqlitesql:** Add templateParameters field for sqlitesql ([#687](https://github.com/googleapis/genai-toolbox/issues/687)) ([75e254c](https://github.com/googleapis/genai-toolbox/commit/75e254c0a4ce690ca5fa4d1741550ce54734b226))
* **tools/valkey:** Add Valkey Source and Tool ([#532](https://github.com/googleapis/genai-toolbox/issues/532)) ([054ec19](https://github.com/googleapis/genai-toolbox/commit/054ec198b97ba9f36f67dd12b2eff0cc6bc4d080))
### Bug Fixes
* **bigquery,mssql:** Fix panic on tools with array param ([#722](https://github.com/googleapis/genai-toolbox/issues/722)) ([7a6644c](https://github.com/googleapis/genai-toolbox/commit/7a6644cf0c5413e5c803955c88a2cfd0a2233ed3))
* **postgres,mssql,cloudsqlmssql:** Encode source connection url for sources ([#727](https://github.com/googleapis/genai-toolbox/issues/727)) ([67964d9](https://github.com/googleapis/genai-toolbox/commit/67964d939f27320b63b5759f4b3f3fdaa0c76fbf)), closes [#717](https://github.com/googleapis/genai-toolbox/issues/717)
* Set default value to field's type during unmarshalling ([#774](https://github.com/googleapis/genai-toolbox/issues/774)) ([fafed24](https://github.com/googleapis/genai-toolbox/commit/fafed2485839cf1acc1350e8a24103d2e6356ee0)), closes [#771](https://github.com/googleapis/genai-toolbox/issues/771)
* **server/mcp:** Do not listen from port for stdio ([#719](https://github.com/googleapis/genai-toolbox/issues/719)) ([d51dbc7](https://github.com/googleapis/genai-toolbox/commit/d51dbc759ba493021d3ec6f5417fc04c21f7044f)), closes [#711](https://github.com/googleapis/genai-toolbox/issues/711)
* **tools/mysqlexecutesql:** Handle nil panic and connection leak in Invoke ([#757](https://github.com/googleapis/genai-toolbox/issues/757)) ([7badba4](https://github.com/googleapis/genai-toolbox/commit/7badba42eefb34252be77b852a57d6bd78dd267d))
* **tools/mysqlsql:** Handle nil panic and connection leak in invoke ([#758](https://github.com/googleapis/genai-toolbox/issues/758)) ([cbb4a33](https://github.com/googleapis/genai-toolbox/commit/cbb4a333517313744800d148840312e56340f3fd))
## [0.7.0](https://github.com/googleapis/genai-toolbox/compare/v0.6.0...v0.7.0) (2025-06-10)

161
README.md
View File

@@ -111,7 +111,7 @@ To install Toolbox as a binary:
<!-- {x-release-please-start-version} -->
```sh
# see releases page for other versions
export VERSION=0.7.0
export VERSION=0.8.0
curl -O https://storage.googleapis.com/genai-toolbox/v$VERSION/linux/amd64/toolbox
chmod +x toolbox
```
@@ -124,7 +124,7 @@ You can also install Toolbox as a container:
```sh
# see releases page for other versions
export VERSION=0.7.0
export VERSION=0.8.0
docker pull us-central1-docker.pkg.dev/database-toolbox/toolbox/toolbox:$VERSION
```
@@ -137,7 +137,7 @@ To install from source, ensure you have the latest version of
[Go installed](https://go.dev/doc/install), and then run the following command:
```sh
go install github.com/googleapis/genai-toolbox@v0.7.0
go install github.com/googleapis/genai-toolbox@v0.8.0
```
<!-- {x-release-please-end} -->
@@ -151,6 +151,8 @@ execute `toolbox` to start the server:
```sh
./toolbox --tools-file "tools.yaml"
```
> [!NOTE]
> Toolbox enables dynamic reloading by default. To disable, use the `--disable-reload` flag.
You can use `toolbox help` for a full list of flags! To stop the server, send a
terminate signal (`ctrl+c` on most platforms).
@@ -165,7 +167,12 @@ Once your server is up and running, you can load the tools into your
application. See below the list of Client SDKs for using various frameworks:
<details open>
<summary>Core</summary>
<summary>Python (<a href="https://github.com/googleapis/mcp-toolbox-sdk-python">Github</a>)</summary>
<br>
<blockquote>
<details open>
<summary>Core</summary>
1. Install [Toolbox Core SDK][toolbox-core]:
@@ -191,9 +198,9 @@ For more detailed instructions on using the Toolbox Core SDK, see the
[toolbox-core]: https://pypi.org/project/toolbox-core/
[toolbox-core-readme]: https://github.com/googleapis/mcp-toolbox-sdk-python/tree/main/packages/toolbox-core/README.md
</details>
<details>
<summary>LangChain / LangGraph</summary>
</details>
<details>
<summary>LangChain / LangGraph</summary>
1. Install [Toolbox LangChain SDK][toolbox-langchain]:
@@ -213,16 +220,15 @@ For more detailed instructions on using the Toolbox Core SDK, see the
tools = client.load_toolset()
```
For more detailed instructions on using the Toolbox LangChain SDK, see the
[project's README][toolbox-langchain-readme].
For more detailed instructions on using the Toolbox LangChain SDK, see the
[project's README][toolbox-langchain-readme].
[toolbox-langchain]: https://pypi.org/project/toolbox-langchain/
[toolbox-langchain-readme]: https://github.com/googleapis/mcp-toolbox-sdk-python/blob/main/packages/toolbox-langchain/README.md
[toolbox-langchain]: https://pypi.org/project/toolbox-langchain/
[toolbox-langchain-readme]: https://github.com/googleapis/mcp-toolbox-sdk-python/blob/main/packages/toolbox-langchain/README.md
</details>
<details>
<summary>LlamaIndex</summary>
</details>
<details>
<summary>LlamaIndex</summary>
1. Install [Toolbox Llamaindex SDK][toolbox-llamaindex]:
@@ -242,12 +248,129 @@ For more detailed instructions on using the Toolbox LangChain SDK, see the
tools = client.load_toolset()
```
For more detailed instructions on using the Toolbox Llamaindex SDK, see the
[project's README][toolbox-llamaindex-readme].
For more detailed instructions on using the Toolbox Llamaindex SDK, see the
[project's README][toolbox-llamaindex-readme].
[toolbox-llamaindex]: https://pypi.org/project/toolbox-llamaindex/
[toolbox-llamaindex-readme]: https://github.com/googleapis/genai-toolbox-llamaindex-python/blob/main/README.md
[toolbox-llamaindex]: https://pypi.org/project/toolbox-llamaindex/
[toolbox-llamaindex-readme]: https://github.com/googleapis/genai-toolbox-llamaindex-python/blob/main/README.md
</details>
</details>
</blockquote>
<details>
<summary>Javascript/Typescript (<a href="https://github.com/googleapis/mcp-toolbox-sdk-js">Github</a>)</summary>
<br>
<blockquote>
<details open>
<summary>Core</summary>
1. Install [Toolbox Core SDK][toolbox-core-js]:
```bash
npm install @toolbox-sdk/core
```
1. Load tools:
```javascript
import { ToolboxClient } from '@toolbox-sdk/core';
// update the url to point to your server
const URL = 'http://127.0.0.1:5000';
let client = new ToolboxClient(URL);
// these tools can be passed to your application!
const tools = await client.loadToolset('toolsetName');
```
For more detailed instructions on using the Toolbox Core SDK, see the
[project's README][toolbox-core-js-readme].
[toolbox-core-js]: https://www.npmjs.com/package/@toolbox-sdk/core
[toolbox-core-js-readme]: https://github.com/googleapis/mcp-toolbox-sdk-js/blob/main/packages/toolbox-core/README.md
</details>
<details>
<summary>LangChain / LangGraph</summary>
1. Install [Toolbox Core SDK][toolbox-core-js]:
```bash
npm install @toolbox-sdk/core
```
2. Load tools:
```javascript
import { ToolboxClient } from '@toolbox-sdk/core';
// update the url to point to your server
const URL = 'http://127.0.0.1:5000';
let client = new ToolboxClient(URL);
// these tools can be passed to your application!
const toolboxTools = await client.loadToolset('toolsetName');
// Define the basics of the tool: name, description, schema and core logic
const getTool = (toolboxTool) => tool(currTool, {
name: toolboxTool.getName(),
description: toolboxTool.getDescription(),
schema: toolboxTool.getParamSchema()
});
// Use these tools in your Langchain/Langraph applications
const tools = toolboxTools.map(getTool);
```
</details>
<details>
<summary>Genkit</summary>
1. Install [Toolbox Core SDK][toolbox-core-js]:
```bash
npm install @toolbox-sdk/core
```
2. Load tools:
```javascript
import { ToolboxClient } from '@toolbox-sdk/core';
import { genkit } from 'genkit';
// Initialise genkit
const ai = genkit({
plugins: [
googleAI({
apiKey: process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY
})
],
model: googleAI.model('gemini-2.0-flash'),
});
// update the url to point to your server
const URL = 'http://127.0.0.1:5000';
let client = new ToolboxClient(URL);
// these tools can be passed to your application!
const toolboxTools = await client.loadToolset('toolsetName');
// Define the basics of the tool: name, description, schema and core logic
const getTool = (toolboxTool) => ai.defineTool({
name: toolboxTool.getName(),
description: toolboxTool.getDescription(),
schema: toolboxTool.getParamSchema()
}, toolboxTool)
// Use these tools in your Genkit applications
const tools = toolboxTools.map(getTool);
```
</details>
</details>
</blockquote>
</details>
## Configuration

View File

@@ -19,20 +19,26 @@ import (
_ "embed"
"fmt"
"io"
"maps"
"os"
"os/signal"
"path/filepath"
"regexp"
"runtime"
"slices"
"strings"
"syscall"
"time"
"github.com/fsnotify/fsnotify"
yaml "github.com/goccy/go-yaml"
"github.com/googleapis/genai-toolbox/internal/auth"
"github.com/googleapis/genai-toolbox/internal/log"
"github.com/googleapis/genai-toolbox/internal/prebuiltconfigs"
"github.com/googleapis/genai-toolbox/internal/server"
"github.com/googleapis/genai-toolbox/internal/sources"
"github.com/googleapis/genai-toolbox/internal/telemetry"
"github.com/googleapis/genai-toolbox/internal/tools"
"github.com/googleapis/genai-toolbox/internal/util"
// Import tool packages for side effect of registration
@@ -178,6 +184,8 @@ func NewCommand(opts ...Option) *Command {
flags.StringVar(&cmd.cfg.TelemetryServiceName, "telemetry-service-name", "toolbox", "Sets the value of the service.name resource attribute for telemetry data.")
flags.StringVar(&cmd.prebuiltConfig, "prebuilt", "", "Use a prebuilt tool configuration by source type. Cannot be used with --tools-file. Allowed: 'alloydb-postgres', 'bigquery', 'cloud-sql-mysql', 'cloud-sql-postgres', 'cloud-sql-mssql', 'postgres', 'spanner', 'spanner-postgres'.")
flags.BoolVar(&cmd.cfg.Stdio, "stdio", false, "Listens via MCP STDIO instead of acting as a remote HTTP server.")
flags.BoolVar(&cmd.cfg.DisableReload, "disable-reload", false, "Disables dynamic reloading of tools file.")
flags.BoolVar(&cmd.cfg.UI, "ui", false, "Launches the Toolbox UI web server.")
// wrap RunE command so that we have access to original Command object
cmd.RunE = func(*cobra.Command, []string) error { return run(cmd) }
@@ -347,7 +355,7 @@ func loadAndMergeToolsFolder(ctx context.Context, folderPath string) (ToolsFile,
// Combine both file lists
allFiles := append(yamlFiles, ymlFiles...)
if len(allFiles) == 0 {
return ToolsFile{}, fmt.Errorf("no YAML files found in directory %q", folderPath)
}
@@ -356,6 +364,177 @@ func loadAndMergeToolsFolder(ctx context.Context, folderPath string) (ToolsFile,
return loadAndMergeToolsFiles(ctx, allFiles)
}
func handleDynamicReload(ctx context.Context, toolsFile ToolsFile, s *server.Server) error {
logger, err := util.LoggerFromContext(ctx)
if err != nil {
panic(err)
}
sourcesMap, authServicesMap, toolsMap, toolsetsMap, err := validateReloadEdits(ctx, toolsFile)
if err != nil {
errMsg := fmt.Errorf("unable to validate reloaded edits: %w", err)
logger.WarnContext(ctx, errMsg.Error())
return err
}
s.ResourceMgr.SetResources(sourcesMap, authServicesMap, toolsMap, toolsetsMap)
return nil
}
// validateReloadEdits checks that the reloaded tools file configs can initialized without failing
func validateReloadEdits(
ctx context.Context, toolsFile ToolsFile,
) (map[string]sources.Source, map[string]auth.AuthService, map[string]tools.Tool, map[string]tools.Toolset, error,
) {
logger, err := util.LoggerFromContext(ctx)
if err != nil {
panic(err)
}
instrumentation, err := util.InstrumentationFromContext(ctx)
if err != nil {
panic(err)
}
logger.DebugContext(ctx, "Attempting to parse and validate reloaded tools file.")
ctx, span := instrumentation.Tracer.Start(ctx, "toolbox/server/reload")
defer span.End()
reloadedConfig := server.ServerConfig{
Version: versionString,
SourceConfigs: toolsFile.Sources,
AuthServiceConfigs: toolsFile.AuthServices,
ToolConfigs: toolsFile.Tools,
ToolsetConfigs: toolsFile.Toolsets,
}
sourcesMap, authServicesMap, toolsMap, toolsetsMap, err := server.InitializeConfigs(ctx, reloadedConfig)
if err != nil {
errMsg := fmt.Errorf("unable to initialize reloaded configs: %w", err)
logger.WarnContext(ctx, errMsg.Error())
return nil, nil, nil, nil, err
}
return sourcesMap, authServicesMap, toolsMap, toolsetsMap, nil
}
// watchChanges checks for changes in the provided yaml tools file(s) or folder.
func watchChanges(ctx context.Context, watchDirs map[string]bool, watchedFiles map[string]bool, s *server.Server) {
logger, err := util.LoggerFromContext(ctx)
if err != nil {
panic(err)
}
w, err := fsnotify.NewWatcher()
if err != nil {
logger.WarnContext(ctx, "error setting up new watcher %s", err)
return
}
defer w.Close()
watchingFolder := false
var folderToWatch string
// if watchedFiles is empty, indicates that user passed entire folder instead
if len(watchedFiles) == 0 {
watchingFolder = true
// validate that watchDirs only has single element
if len(watchDirs) > 1 {
logger.WarnContext(ctx, "error setting watcher, expected single tools folder if no file(s) are defined.")
return
}
for onlyKey := range watchDirs {
folderToWatch = onlyKey
break
}
}
for dir := range watchDirs {
err := w.Add(dir)
if err != nil {
logger.WarnContext(ctx, fmt.Sprintf("Error adding path %s to watcher: %s", dir, err))
break
}
logger.DebugContext(ctx, fmt.Sprintf("Added directory %s to watcher.", dir))
}
// debounce timer is used to prevent multiple writes triggering multiple reloads
debounceDelay := 100 * time.Millisecond
debounce := time.NewTimer(1 * time.Minute)
debounce.Stop()
for {
select {
case <-ctx.Done():
logger.DebugContext(ctx, "file watcher context cancelled")
return
case err, ok := <-w.Errors:
if !ok {
logger.WarnContext(ctx, "file watcher was closed unexpectedly")
return
}
if err != nil {
logger.WarnContext(ctx, "file watcher error %s", err)
return
}
case e, ok := <-w.Events:
if !ok {
logger.WarnContext(ctx, "file watcher already closed")
return
}
// only check for write events which indicate user saved a new tools file
if !e.Has(fsnotify.Write) {
continue
}
cleanedFilename := filepath.Clean(e.Name)
logger.DebugContext(ctx, fmt.Sprintf("WRITE event detected in %s", cleanedFilename))
folderChanged := watchingFolder &&
(strings.HasSuffix(cleanedFilename, ".yaml") || strings.HasSuffix(cleanedFilename, ".yml"))
if folderChanged || watchedFiles[cleanedFilename] {
// indicates the write event is on a relevant file
debounce.Reset(debounceDelay)
}
case <-debounce.C:
debounce.Stop()
var reloadedToolsFile ToolsFile
if watchingFolder {
logger.DebugContext(ctx, "Reloading tools folder.")
reloadedToolsFile, err = loadAndMergeToolsFolder(ctx, folderToWatch)
if err != nil {
logger.WarnContext(ctx, "error loading tools folder %s", err)
continue
}
} else {
logger.DebugContext(ctx, "Reloading tools file(s).")
reloadedToolsFile, err = loadAndMergeToolsFiles(ctx, slices.Collect(maps.Keys(watchedFiles)))
if err != nil {
logger.WarnContext(ctx, "error loading tools files %s", err)
continue
}
}
err = handleDynamicReload(ctx, reloadedToolsFile, s)
if err != nil {
errMsg := fmt.Errorf("unable to parse reloaded tools file at %q: %w", reloadedToolsFile, err)
logger.WarnContext(ctx, errMsg.Error())
continue
}
}
}
}
// updateLogLevel checks if Toolbox have to update the existing log level set by users.
// stdio doesn't support "debug" and "info" logs.
func updateLogLevel(stdio bool, logLevel string) bool {
@@ -370,6 +549,33 @@ func updateLogLevel(stdio bool, logLevel string) bool {
return false
}
func resolveWatcherInputs(toolsFile string, toolsFiles []string, toolsFolder string) (map[string]bool, map[string]bool) {
var relevantFiles []string
// map for efficiently checking if a file is relevant
watchedFiles := make(map[string]bool)
// dirs that will be added to watcher (fsnotify prefers watching directory then filtering for file)
watchDirs := make(map[string]bool)
if len(toolsFiles) > 0 {
relevantFiles = toolsFiles
} else if toolsFolder != "" {
watchDirs[filepath.Clean(toolsFolder)] = true
} else {
relevantFiles = []string{toolsFile}
}
// extract parent dir for relevant files and dedup
for _, f := range relevantFiles {
cleanFile := filepath.Clean(f)
watchedFiles[cleanFile] = true
watchDirs[filepath.Dir(cleanFile)] = true
}
return watchDirs, watchedFiles
}
func run(cmd *Command) error {
if updateLogLevel(cmd.cfg.Stdio, cmd.cfg.LogLevel.String()) {
cmd.cfg.LogLevel = server.StringLevel(log.Warn)
@@ -466,6 +672,7 @@ func run(cmd *Command) error {
cmd.logger.ErrorContext(ctx, errMsg.Error())
return errMsg
}
// Use multiple tools files
cmd.logger.InfoContext(ctx, fmt.Sprintf("Loading and merging %d tool configuration files", len(cmd.tools_files)))
var err error
@@ -481,6 +688,7 @@ func run(cmd *Command) error {
cmd.logger.ErrorContext(ctx, errMsg.Error())
return errMsg
}
// Use tools folder
cmd.logger.InfoContext(ctx, fmt.Sprintf("Loading and merging all YAML files from directory: %s", cmd.tools_folder))
var err error
@@ -494,6 +702,7 @@ func run(cmd *Command) error {
if cmd.tools_file == "" {
cmd.tools_file = "tools.yaml"
}
// Read single tool file contents
buf, err := os.ReadFile(cmd.tools_file)
if err != nil {
@@ -516,9 +725,23 @@ func run(cmd *Command) error {
cmd.logger.WarnContext(ctx, "`authSources` is deprecated, use `authServices` instead")
cmd.cfg.AuthServiceConfigs = authSourceConfigs
}
if err != nil {
errMsg := fmt.Errorf("unable to parse tool file at %q: %w", cmd.tools_file, err)
cmd.logger.ErrorContext(ctx, errMsg.Error())
return errMsg
}
instrumentation, err := telemetry.CreateTelemetryInstrumentation(versionString)
if err != nil {
errMsg := fmt.Errorf("unable to create telemetry instrumentation: %w", err)
cmd.logger.ErrorContext(ctx, errMsg.Error())
return errMsg
}
ctx = util.WithInstrumentation(ctx, instrumentation)
// start server
s, err := server.NewServer(ctx, cmd.cfg, cmd.logger)
s, err := server.NewServer(ctx, cmd.cfg)
if err != nil {
errMsg := fmt.Errorf("toolbox failed to initialize: %w", err)
cmd.logger.ErrorContext(ctx, errMsg.Error())
@@ -543,6 +766,9 @@ func run(cmd *Command) error {
return errMsg
}
cmd.logger.InfoContext(ctx, "Server ready to serve!")
if cmd.cfg.UI {
cmd.logger.InfoContext(ctx, "Toolbox UI is up and running at: http://localhost:5000/ui")
}
go func() {
defer close(srvErr)
@@ -553,6 +779,13 @@ func run(cmd *Command) error {
}()
}
watchDirs, watchedFiles := resolveWatcherInputs(cmd.tools_file, cmd.tools_files, cmd.tools_folder)
if !cmd.cfg.DisableReload {
// start watching the file(s) or folder for changes to trigger dynamic reloading
go watchChanges(ctx, watchDirs, watchedFiles, s)
}
// wait for either the server to error out or the command's context to be canceled
select {
case err := <-srvErr:

View File

@@ -16,23 +16,33 @@ package cmd
import (
"bytes"
"context"
_ "embed"
"fmt"
"io"
"os"
"path"
"path/filepath"
"regexp"
"runtime"
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/googleapis/genai-toolbox/internal/auth/google"
"github.com/googleapis/genai-toolbox/internal/log"
"github.com/googleapis/genai-toolbox/internal/prebuiltconfigs"
"github.com/googleapis/genai-toolbox/internal/server"
cloudsqlpgsrc "github.com/googleapis/genai-toolbox/internal/sources/cloudsqlpg"
httpsrc "github.com/googleapis/genai-toolbox/internal/sources/http"
"github.com/googleapis/genai-toolbox/internal/telemetry"
"github.com/googleapis/genai-toolbox/internal/testutils"
"github.com/googleapis/genai-toolbox/internal/tools"
"github.com/googleapis/genai-toolbox/internal/tools/http"
"github.com/googleapis/genai-toolbox/internal/tools/postgres/postgressql"
"github.com/googleapis/genai-toolbox/internal/util"
"github.com/spf13/cobra"
)
@@ -174,6 +184,13 @@ func TestServerConfigFlags(t *testing.T) {
Stdio: true,
}),
},
{
desc: "disable reload",
args: []string{"--disable-reload"},
want: withDefaults(server.ServerConfig{
DisableReload: true,
}),
},
}
for _, tc := range tcs {
t.Run(tc.desc, func(t *testing.T) {
@@ -965,6 +982,183 @@ func TestEnvVarReplacement(t *testing.T) {
}
// normalizeFilepaths is a helper function to allow same filepath formats for Mac and Windows.
// this prevents needing multiple "want" cases for TestResolveWatcherInputs
func normalizeFilepaths(m map[string]bool) map[string]bool {
newMap := make(map[string]bool)
for k, v := range m {
newMap[filepath.ToSlash(k)] = v
}
return newMap
}
func TestResolveWatcherInputs(t *testing.T) {
tcs := []struct {
description string
toolsFile string
toolsFiles []string
toolsFolder string
wantWatchDirs map[string]bool
wantWatchedFiles map[string]bool
}{
{
description: "single tools file",
toolsFile: "tools_folder/example_tools.yaml",
toolsFiles: []string{},
toolsFolder: "",
wantWatchDirs: map[string]bool{"tools_folder": true},
wantWatchedFiles: map[string]bool{"tools_folder/example_tools.yaml": true},
},
{
description: "default tools file (root dir)",
toolsFile: "tools.yaml",
toolsFiles: []string{},
toolsFolder: "",
wantWatchDirs: map[string]bool{".": true},
wantWatchedFiles: map[string]bool{"tools.yaml": true},
},
{
description: "multiple files in different folders",
toolsFile: "",
toolsFiles: []string{"tools_folder/example_tools.yaml", "tools_folder2/example_tools.yaml"},
toolsFolder: "",
wantWatchDirs: map[string]bool{"tools_folder": true, "tools_folder2": true},
wantWatchedFiles: map[string]bool{
"tools_folder/example_tools.yaml": true,
"tools_folder2/example_tools.yaml": true,
},
},
{
description: "multiple files in same folder",
toolsFile: "",
toolsFiles: []string{"tools_folder/example_tools.yaml", "tools_folder/example_tools2.yaml"},
toolsFolder: "",
wantWatchDirs: map[string]bool{"tools_folder": true},
wantWatchedFiles: map[string]bool{
"tools_folder/example_tools.yaml": true,
"tools_folder/example_tools2.yaml": true,
},
},
{
description: "multiple files in different levels",
toolsFile: "",
toolsFiles: []string{
"tools_folder/example_tools.yaml",
"tools_folder/special_tools/example_tools2.yaml"},
toolsFolder: "",
wantWatchDirs: map[string]bool{"tools_folder": true, "tools_folder/special_tools": true},
wantWatchedFiles: map[string]bool{
"tools_folder/example_tools.yaml": true,
"tools_folder/special_tools/example_tools2.yaml": true,
},
},
{
description: "tools folder",
toolsFile: "",
toolsFiles: []string{},
toolsFolder: "tools_folder",
wantWatchDirs: map[string]bool{"tools_folder": true},
wantWatchedFiles: map[string]bool{},
},
}
for _, tc := range tcs {
t.Run(tc.description, func(t *testing.T) {
gotWatchDirs, gotWatchedFiles := resolveWatcherInputs(tc.toolsFile, tc.toolsFiles, tc.toolsFolder)
normalizedGotWatchDirs := normalizeFilepaths(gotWatchDirs)
normalizedGotWatchedFiles := normalizeFilepaths(gotWatchedFiles)
if diff := cmp.Diff(tc.wantWatchDirs, normalizedGotWatchDirs); diff != "" {
t.Errorf("incorrect watchDirs: diff %v", diff)
}
if diff := cmp.Diff(tc.wantWatchedFiles, normalizedGotWatchedFiles); diff != "" {
t.Errorf("incorrect watchedFiles: diff %v", diff)
}
})
}
}
// helper function for testing file detection in dynamic reloading
func tmpFileWithCleanup(content []byte) (string, func(), error) {
f, err := os.CreateTemp("", "*")
if err != nil {
return "", nil, err
}
cleanup := func() { os.Remove(f.Name()) }
if _, err := f.Write(content); err != nil {
cleanup()
return "", nil, err
}
if err := f.Close(); err != nil {
cleanup()
return "", nil, err
}
return f.Name(), cleanup, err
}
func TestSingleEdit(t *testing.T) {
ctx, cancelCtx := context.WithTimeout(context.Background(), time.Minute)
defer cancelCtx()
pr, pw := io.Pipe()
defer pw.Close()
defer pr.Close()
fileToWatch, cleanup, err := tmpFileWithCleanup([]byte("initial content"))
if err != nil {
t.Fatalf("error editing tools file %s", err)
}
defer cleanup()
logger, err := log.NewStdLogger(pw, pw, "DEBUG")
if err != nil {
t.Fatalf("failed to setup logger %s", err)
}
ctx = util.WithLogger(ctx, logger)
instrumentation, err := telemetry.CreateTelemetryInstrumentation(versionString)
if err != nil {
t.Fatalf("failed to setup instrumentation %s", err)
}
ctx = util.WithInstrumentation(ctx, instrumentation)
mockServer := &server.Server{}
cleanFileToWatch := filepath.Clean(fileToWatch)
watchDir := filepath.Dir(cleanFileToWatch)
watchedFiles := map[string]bool{cleanFileToWatch: true}
watchDirs := map[string]bool{watchDir: true}
go watchChanges(ctx, watchDirs, watchedFiles, mockServer)
// escape backslash so regex doesn't fail on windows filepaths
regexEscapedPathFile := strings.ReplaceAll(cleanFileToWatch, `\`, `\\\\*\\`)
regexEscapedPathFile = path.Clean(regexEscapedPathFile)
regexEscapedPathDir := strings.ReplaceAll(watchDir, `\`, `\\\\*\\`)
regexEscapedPathDir = path.Clean(regexEscapedPathDir)
begunWatchingDir := regexp.MustCompile(fmt.Sprintf(`DEBUG "Added directory %s to watcher."`, regexEscapedPathDir))
_, err = testutils.WaitForString(ctx, begunWatchingDir, pr)
if err != nil {
t.Fatalf("timeout or error waiting for watcher to start: %s", err)
}
err = os.WriteFile(fileToWatch, []byte("modification"), 0777)
if err != nil {
t.Fatalf("error writing to file: %v", err)
}
detectedFileChange := regexp.MustCompile(fmt.Sprintf(`DEBUG "WRITE event detected in %s"`, regexEscapedPathFile))
_, err = testutils.WaitForString(ctx, detectedFileChange, pr)
if err != nil {
t.Fatalf("timeout or error waiting for file to detect write: %s", err)
}
}
func TestPrebuiltTools(t *testing.T) {
alloydb_config, _ := prebuiltconfigs.Get("alloydb-postgres")
bigquery_config, _ := prebuiltconfigs.Get("bigquery")

View File

@@ -1 +1 @@
0.7.0
0.8.0

View File

@@ -222,7 +222,7 @@
},
"outputs": [],
"source": [
"version = \"0.7.0\" # x-release-please-version\n",
"version = \"0.8.0\" # x-release-please-version\n",
"! curl -O https://storage.googleapis.com/genai-toolbox/v{version}/linux/amd64/toolbox\n",
"\n",
"# Make the binary executable\n",

View File

@@ -86,7 +86,7 @@ To install Toolbox as a binary:
```sh
# see releases page for other versions
export VERSION=0.7.0
export VERSION=0.8.0
curl -O https://storage.googleapis.com/genai-toolbox/v$VERSION/linux/amd64/toolbox
chmod +x toolbox
```
@@ -97,7 +97,7 @@ You can also install Toolbox as a container:
```sh
# see releases page for other versions
export VERSION=0.7.0
export VERSION=0.8.0
docker pull us-central1-docker.pkg.dev/database-toolbox/toolbox/toolbox:$VERSION
```
@@ -108,7 +108,7 @@ To install from source, ensure you have the latest version of
[Go installed](https://go.dev/doc/install), and then run the following command:
```sh
go install github.com/googleapis/genai-toolbox@v0.7.0
go install github.com/googleapis/genai-toolbox@v0.8.0
```
{{% /tab %}}
@@ -123,6 +123,9 @@ execute `toolbox` to start the server:
```sh
./toolbox --tools-file "tools.yaml"
```
{{< notice note >}}
Toolbox enables dynamic reloading by default. To disable, use the `--disable-reload` flag.
{{< /notice >}}
You can use `toolbox help` for a full list of flags! To stop the server, send a
terminate signal (`ctrl+c` on most platforms).
@@ -135,6 +138,7 @@ out the resources in the [How-to section](../../how-to/_index.md)
Once your server is up and running, you can load the tools into your
application. See below the list of Client SDKs for using various frameworks:
#### Python
{{< tabpane text=true persist=header >}}
{{% tab header="Core" lang="en" %}}
@@ -201,3 +205,115 @@ For more detailed instructions on using the Toolbox Llamaindex SDK, see the
{{% /tab %}}
{{< /tabpane >}}
#### Javascript/Typescript
Once you've installed the [Toolbox Core
SDK](https://www.npmjs.com/package/@toolbox-sdk/core), you can load
tools:
{{< tabpane text=true persist=header >}}
{{% tab header="Core" lang="en" %}}
{{< highlight javascript >}}
import { ToolboxClient } from '@toolbox-sdk/core';
// update the url to point to your server
const URL = 'http://127.0.0.1:5000';
let client = new ToolboxClient(URL);
// these tools can be passed to your application!
const toolboxTools = await client.loadToolset('toolsetName');
{{< /highlight >}}
{{% /tab %}}
{{% tab header="LangChain/Langraph" lang="en" %}}
{{< highlight javascript >}}
import { ToolboxClient } from '@toolbox-sdk/core';
// update the url to point to your server
const URL = 'http://127.0.0.1:5000';
let client = new ToolboxClient(URL);
// these tools can be passed to your application!
const toolboxTools = await client.loadToolset('toolsetName');
// Define the basics of the tool: name, description, schema and core logic
const getTool = (toolboxTool) => tool(currTool, {
name: toolboxTool.getName(),
description: toolboxTool.getDescription(),
schema: toolboxTool.getParamSchema()
});
// Use these tools in your Langchain/Langraph applications
const tools = toolboxTools.map(getTool);
{{< /highlight >}}
{{% /tab %}}
{{% tab header="Genkit" lang="en" %}}
{{< highlight javascript >}}
import { ToolboxClient } from '@toolbox-sdk/core';
import { genkit } from 'genkit';
// Initialise genkit
const ai = genkit({
plugins: [
googleAI({
apiKey: process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY
})
],
model: googleAI.model('gemini-2.0-flash'),
});
// update the url to point to your server
const URL = 'http://127.0.0.1:5000';
let client = new ToolboxClient(URL);
// these tools can be passed to your application!
const toolboxTools = await client.loadToolset('toolsetName');
// Define the basics of the tool: name, description, schema and core logic
const getTool = (toolboxTool) => ai.defineTool({
name: toolboxTool.getName(),
description: toolboxTool.getDescription(),
schema: toolboxTool.getParamSchema()
}, toolboxTool)
// Use these tools in your Genkit applications
const tools = toolboxTools.map(getTool);
{{< /highlight >}}
{{% /tab %}}
{{% tab header="LlamaIndex" lang="en" %}}
{{< highlight javascript >}}
import { ToolboxClient } from '@toolbox-sdk/core';
import { tool } from "llamaindex";
// update the url to point to your server
const URL = 'http://127.0.0.1:5000';
let client = new ToolboxClient(URL);
// these tools can be passed to your application!
const toolboxTools = await client.loadToolset('toolsetName');
// Define the basics of the tool: name, description, schema and core logic
const getTool = (toolboxTool) => tool({
name: toolboxTool.getName(),
description: toolboxTool.getDescription(),
parameters: toolboxTool.getParams(),
execute: toolboxTool
});;
// Use these tools in your LlamaIndex applications
const tools = toolboxTools.map(getTool);
{{< /highlight >}}
{{% /tab %}}
{{< /tabpane >}}
For more detailed instructions on using the Toolbox Core SDK, see the
[project's README](https://github.com/googleapis/mcp-toolbox-sdk-js/blob/main/packages/toolbox-core/README.md).

View File

@@ -156,7 +156,7 @@ In this section, we will download Toolbox, configure our tools in a
<!-- {x-release-please-start-version} -->
```bash
export OS="linux/amd64" # one of linux/amd64, darwin/arm64, darwin/amd64, or windows/amd64
curl -O https://storage.googleapis.com/genai-toolbox/v0.7.0/$OS/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.8.0/$OS/toolbox
```
<!-- {x-release-please-end} -->
@@ -257,6 +257,9 @@ In this section, we will download Toolbox, configure our tools in a
```bash
./toolbox --tools-file "tools.yaml"
```
{{< notice note >}}
Toolbox enables dynamic reloading by default. To disable, use the `--disable-reload` flag.
{{< /notice >}}
## Step 3: Connect your agent to Toolbox

View File

@@ -105,7 +105,7 @@ In this section, we will download Toolbox, configure our tools in a
<!-- {x-release-please-start-version} -->
```bash
export OS="linux/amd64" # one of linux/amd64, darwin/arm64, darwin/amd64, or windows/amd64
curl -O https://storage.googleapis.com/genai-toolbox/v0.7.0/$OS/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.8.0/$OS/toolbox
```
<!-- {x-release-please-end} -->

View File

@@ -52,19 +52,19 @@ Omni](https://cloud.google.com/alloydb/omni/current/docs/overview).
<!-- {x-release-please-start-version} -->
{{< tabpane persist=header >}}
{{< tab header="linux/amd64" lang="bash" >}}
curl -O <https://storage.googleapis.com/genai-toolbox/v0.7.0/linux/amd64/toolbox>
curl -O <https://storage.googleapis.com/genai-toolbox/v0.8.0/linux/amd64/toolbox>
{{< /tab >}}
{{< tab header="darwin/arm64" lang="bash" >}}
curl -O <https://storage.googleapis.com/genai-toolbox/v0.7.0/darwin/arm64/toolbox>
curl -O <https://storage.googleapis.com/genai-toolbox/v0.8.0/darwin/arm64/toolbox>
{{< /tab >}}
{{< tab header="darwin/amd64" lang="bash" >}}
curl -O <https://storage.googleapis.com/genai-toolbox/v0.7.0/darwin/amd64/toolbox>
curl -O <https://storage.googleapis.com/genai-toolbox/v0.8.0/darwin/amd64/toolbox>
{{< /tab >}}
{{< tab header="windows/amd64" lang="bash" >}}
curl -O <https://storage.googleapis.com/genai-toolbox/v0.7.0/windows/amd64/toolbox>
curl -O <https://storage.googleapis.com/genai-toolbox/v0.8.0/windows/amd64/toolbox>
{{< /tab >}}
{{< /tabpane >}}
<!-- {x-release-please-end} -->

View File

@@ -63,6 +63,10 @@ When running with stdio, Toolbox will listen via stdio instead of acting as a
remote HTTP server. Logs will be set to the `warn` level by default. `debug` and
`info` logs are not supported with stdio.
{{< notice note >}}
Toolbox enables dynamic reloading by default. To disable, use the `--disable-reload` flag.
{{< /notice >}}
### Connecting via HTTP
Toolbox supports the HTTP transport protocol with and without SSE.

View File

@@ -79,7 +79,7 @@ database are in the same VPC network.
Create a `tools.yaml` file that contains your configuration for Toolbox. For
details, see the
[configuration](https://github.com/googleapis/genai-toolbox/blob/main/README.md#configuration)
[configuration](https://googleapis.github.io/genai-toolbox/resources/sources/)
section.
## Deploy to Cloud Run

View File

@@ -15,8 +15,10 @@ It's compatible with the following sources:
- [bigquery](../sources/bigquery.md)
bigquery-get-dataset-info takes a dataset parameter to specify the dataset
on the given source.
`bigquery-get-dataset-info` takes a `dataset` parameter to specify the dataset
on the given source. It also optionally accepts a `project` parameter to
define the Google Cloud project ID. If the `project` parameter is not provided,
the tool defaults to using the project defined in the source configuration.
## Example

View File

@@ -15,8 +15,10 @@ It's compatible with the following sources:
- [bigquery](../sources/bigquery.md)
bigquery-get-table-info takes dataset and table parameters to specify
the target table.
`bigquery-get-table-info` takes `dataset` and `table` parameters to specify
the target table. It also optionally accepts a `project` parameter to define
the Google Cloud project ID. If the `project` parameter is not provided, the
tool defaults to using the project defined in the source configuration.
## Example

View File

@@ -15,8 +15,9 @@ It's compatible with the following sources:
- [bigquery](../sources/bigquery.md)
bigquery-list-dataset-ids requires no input parameters beyond the configured
source.
`bigquery-list-dataset-ids` optionally accepts a `project` parameter to define
the Google Cloud project ID. If the `project` parameter is not provided, the
tool defaults to using the project defined in the source configuration.
## Example

View File

@@ -15,8 +15,10 @@ It's compatible with the following sources:
- [bigquery](../sources/bigquery.md)
bigquery-get-dataset-info takes a dataset parameter to specify the dataset
from which to list table IDs.
`bigquery-get-dataset-info` takes a required `dataset` parameter to specify the dataset
from which to list table IDs. It also optionally accepts a `project` parameter to
define the Google Cloud project ID. If the `project` parameter is not provided, the
tool defaults to using the project defined in the source configuration.
## Example

View File

@@ -220,7 +220,7 @@
},
"outputs": [],
"source": [
"version = \"0.7.0\" # x-release-please-version\n",
"version = \"0.8.0\" # x-release-please-version\n",
"! curl -O https://storage.googleapis.com/genai-toolbox/v{version}/linux/amd64/toolbox\n",
"\n",
"# Make the binary executable\n",

View File

@@ -179,7 +179,7 @@ to use BigQuery, and then run the Toolbox server.
<!-- {x-release-please-start-version} -->
```bash
export OS="linux/amd64" # one of linux/amd64, darwin/arm64, darwin/amd64, or windows/amd64
curl -O https://storage.googleapis.com/genai-toolbox/v0.7.0/$OS/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.8.0/$OS/toolbox
```
<!-- {x-release-please-end} -->
@@ -292,6 +292,9 @@ to use BigQuery, and then run the Toolbox server.
```bash
./toolbox --tools-file "tools.yaml"
```
{{< notice note >}}
Toolbox enables dynamic reloading by default. To disable, use the `--disable-reload` flag.
{{< /notice >}}
## Step 3: Connect your agent to Toolbox

View File

@@ -98,7 +98,7 @@ In this section, we will download Toolbox, configure our tools in a
<!-- {x-release-please-start-version} -->
```bash
export OS="linux/amd64" # one of linux/amd64, darwin/arm64, darwin/amd64, or windows/amd64
curl -O https://storage.googleapis.com/genai-toolbox/v0.7.0/$OS/toolbox
curl -O https://storage.googleapis.com/genai-toolbox/v0.8.0/$OS/toolbox
```
<!-- {x-release-please-end} -->

24
go.mod
View File

@@ -1,33 +1,37 @@
module github.com/googleapis/genai-toolbox
go 1.23.8
go 1.24.2
toolchain go1.24.4
require (
cloud.google.com/go/alloydbconn v1.15.3
cloud.google.com/go/bigquery v1.69.0
cloud.google.com/go/bigtable v1.37.0
cloud.google.com/go/bigtable v1.38.0
cloud.google.com/go/cloudsqlconn v1.17.2
cloud.google.com/go/spanner v1.83.0
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.29.0
github.com/couchbase/gocb/v2 v2.10.0
github.com/couchbase/tools-common/http v1.0.9
github.com/fsnotify/fsnotify v1.9.0
github.com/go-chi/chi/v5 v5.2.2
github.com/go-chi/httplog/v2 v2.1.1
github.com/go-chi/render v1.0.3
github.com/go-playground/validator/v10 v10.26.0
github.com/go-goquery/goquery v1.0.1
github.com/go-playground/validator/v10 v10.27.0
github.com/go-sql-driver/mysql v1.9.3
github.com/goccy/go-yaml v1.18.0
github.com/google/go-cmp v0.7.0
github.com/google/uuid v1.6.0
github.com/googleapis/mcp-toolbox-sdk-go v0.2.0
github.com/jackc/pgx/v5 v5.7.5
github.com/json-iterator/go v1.1.12
github.com/microsoft/go-mssqldb v1.9.2
github.com/neo4j/neo4j-go-driver/v5 v5.28.1
github.com/redis/go-redis/v9 v9.11.0
github.com/spf13/cobra v1.9.1
github.com/tmc/langchaingo v0.1.13
github.com/valkey-io/valkey-go v1.0.62
go.opentelemetry.io/contrib/propagators/autoprop v0.61.0
go.opentelemetry.io/otel v1.36.0
@@ -38,7 +42,7 @@ require (
go.opentelemetry.io/otel/sdk/metric v1.36.0
go.opentelemetry.io/otel/trace v1.36.0
golang.org/x/oauth2 v0.30.0
google.golang.org/api v0.239.0
google.golang.org/api v0.242.0
modernc.org/sqlite v1.38.0
)
@@ -47,6 +51,8 @@ require golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
require (
cel.dev/expr v0.23.0 // indirect
cloud.google.com/go v0.121.2 // indirect
cloud.google.com/go/ai v0.7.0 // indirect
cloud.google.com/go/aiplatform v1.85.0 // indirect
cloud.google.com/go/alloydb v1.16.1 // indirect
cloud.google.com/go/auth v0.16.2 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
@@ -55,11 +61,14 @@ require (
cloud.google.com/go/longrunning v0.6.7 // indirect
cloud.google.com/go/monitoring v1.24.2 // indirect
cloud.google.com/go/trace v1.11.6 // indirect
cloud.google.com/go/vertexai v0.12.0 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.3 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 // indirect
github.com/PuerkitoBio/goquery v1.10.3 // indirect
github.com/ajg/form v1.5.1 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/apache/arrow/go/v15 v15.0.2 // indirect
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
@@ -70,12 +79,13 @@ require (
github.com/couchbase/tools-common/errors v1.0.0 // indirect
github.com/couchbaselabs/gocbconnstr/v2 v2.0.0-20240607131231-fb385523de28 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dlclark/regexp2 v1.10.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
github.com/go-jose/go-jose/v4 v4.1.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-playground/locales v0.14.1 // indirect
@@ -86,6 +96,7 @@ require (
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/flatbuffers v23.5.26+incompatible // indirect
github.com/google/generative-ai-go v0.15.1 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
github.com/googleapis/gax-go/v2 v2.14.2 // indirect
@@ -96,7 +107,7 @@ require (
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/klauspost/compress v1.16.7 // indirect
github.com/klauspost/compress v1.17.6 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
@@ -104,6 +115,7 @@ require (
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/pierrec/lz4/v4 v4.1.18 // indirect
github.com/pkoukk/tiktoken-go v0.1.6 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/spf13/pflag v1.0.6 // indirect

88
go.sum
View File

@@ -47,12 +47,16 @@ cloud.google.com/go/accesscontextmanager v1.3.0/go.mod h1:TgCBehyr5gNMz7ZaH9xubp
cloud.google.com/go/accesscontextmanager v1.4.0/go.mod h1:/Kjh7BBu/Gh83sv+K60vN9QE5NJcd80sU33vIe2IFPE=
cloud.google.com/go/accesscontextmanager v1.6.0/go.mod h1:8XCvZWfYw3K/ji0iVnp+6pu7huxoQTLmxAbVjbloTtM=
cloud.google.com/go/accesscontextmanager v1.7.0/go.mod h1:CEGLewx8dwa33aDAZQujl7Dx+uYhS0eay198wB/VumQ=
cloud.google.com/go/ai v0.7.0 h1:P6+b5p4gXlza5E+u7uvcgYlzZ7103ACg70YdZeC6oGE=
cloud.google.com/go/ai v0.7.0/go.mod h1:7ozuEcraovh4ABsPbrec3o4LmFl9HigNI3D5haxYeQo=
cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw=
cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY=
cloud.google.com/go/aiplatform v1.27.0/go.mod h1:Bvxqtl40l0WImSb04d0hXFU7gDOiq9jQmorivIiWcKg=
cloud.google.com/go/aiplatform v1.35.0/go.mod h1:7MFT/vCaOyZT/4IIFfxH4ErVg/4ku6lKv3w0+tFTgXQ=
cloud.google.com/go/aiplatform v1.36.1/go.mod h1:WTm12vJRPARNvJ+v6P52RDHCNe4AhvjcIZ/9/RRHy/k=
cloud.google.com/go/aiplatform v1.37.0/go.mod h1:IU2Cv29Lv9oCn/9LkFiiuKfwrRTq+QQMbW+hPCxJGZw=
cloud.google.com/go/aiplatform v1.85.0 h1:80/GqdP8Tovaaw9Qr6fYZNDvwJeA9rLk8mYkqBJNIJQ=
cloud.google.com/go/aiplatform v1.85.0/go.mod h1:S4DIKz3TFLSt7ooF2aCRdAqsUR4v/YDXUoHqn5P0EFc=
cloud.google.com/go/alloydb v1.16.1 h1:pW4D0O2jAfAjoOEI1bgChPwMHWE8X8BjwSO0tfWkWvk=
cloud.google.com/go/alloydb v1.16.1/go.mod h1:zeZuGJ5mEaQE70FMXEvZIp5hQLR9yrGnHo1YUOncWRY=
cloud.google.com/go/alloydbconn v1.15.3 h1:j0Y0+LpVjdyUguX0uwsaeTtq4tQUZiFvsO52AH+yusY=
@@ -139,8 +143,8 @@ cloud.google.com/go/bigquery v1.49.0/go.mod h1:Sv8hMmTFFYBlt/ftw2uN6dFdQPzBlREY9
cloud.google.com/go/bigquery v1.50.0/go.mod h1:YrleYEh2pSEbgTBZYMJ5SuSr0ML3ypjRB1zgf7pvQLU=
cloud.google.com/go/bigquery v1.69.0 h1:rZvHnjSUs5sHK3F9awiuFk2PeOaB8suqNuim21GbaTc=
cloud.google.com/go/bigquery v1.69.0/go.mod h1:TdGLquA3h/mGg+McX+GsqG9afAzTAcldMjqhdjHTLew=
cloud.google.com/go/bigtable v1.37.0 h1:Q+x7y04lQ0B+WXp03wc1/FLhFt4CwcQdkwWT0M4Jp3w=
cloud.google.com/go/bigtable v1.37.0/go.mod h1:HXqddP6hduwzrtiTCqZPpj9ij4hGZb4Zy1WF/dT+yaU=
cloud.google.com/go/bigtable v1.38.0 h1:L/PnUXRtAzFfa7qMULJHt4cXa/O2dqPJEkzYNGA4hfo=
cloud.google.com/go/bigtable v1.38.0/go.mod h1:o/lntJarF3Y5C0XYLMJLjLYwxaRbcrtM0BiV57ymXbI=
cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY=
cloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s=
cloud.google.com/go/billing v1.6.0/go.mod h1:WoXzguj+BeHXPbKfNWkqVtDdzORazmCjraY+vrxcyvI=
@@ -502,6 +506,8 @@ cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISI
cloud.google.com/go/secretmanager v1.8.0/go.mod h1:hnVgi/bN5MYHd3Gt0SPuTPPp5ENina1/LxM+2W9U9J4=
cloud.google.com/go/secretmanager v1.9.0/go.mod h1:b71qH2l1yHmWQHt9LC80akm86mX8AL6X1MA01dW8ht4=
cloud.google.com/go/secretmanager v1.10.0/go.mod h1:MfnrdvKMPNra9aZtQFvBcvRU54hbPD8/HayQdlUgJpU=
cloud.google.com/go/secretmanager v1.15.0 h1:RtkCMgTpaBMbzozcRUGfZe46jb9a3qh5EdEtVRUATF8=
cloud.google.com/go/secretmanager v1.15.0/go.mod h1:1hQSAhKK7FldiYw//wbR/XPfPc08eQ81oBsnRUHEvUc=
cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4=
cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0=
cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU=
@@ -559,8 +565,8 @@ cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeL
cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s=
cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y=
cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjpS5GBrB8h4=
cloud.google.com/go/storage v1.53.0 h1:gg0ERZwL17pJ+Cz3cD2qS60w1WMDnwcm5YPAIQBHUAw=
cloud.google.com/go/storage v1.53.0/go.mod h1:7/eO2a/srr9ImZW9k5uufcNahT2+fPb8w5it1i5boaA=
cloud.google.com/go/storage v1.55.0 h1:NESjdAToN9u1tmhVqhXCaCwYBuvEhZLLv0gBr+2znf0=
cloud.google.com/go/storage v1.55.0/go.mod h1:ztSmTTwzsdXe5syLVS0YsbFxXuvEmEyZj7v7zChEmuY=
cloud.google.com/go/storagetransfer v1.5.0/go.mod h1:dxNzUopWy7RQevYFHewchb29POFv3/AaBgnhqzqiK0w=
cloud.google.com/go/storagetransfer v1.6.0/go.mod h1:y77xm4CQV/ZhFZH75PLEXY0ROiS7Gh6pSKrM8dJyg6I=
cloud.google.com/go/storagetransfer v1.7.0/go.mod h1:8Giuj1QNb1kfLAiWM1bN6dHzfdlDAVC9rv9abHot2W4=
@@ -587,6 +593,8 @@ cloud.google.com/go/translate v1.4.0/go.mod h1:06Dn/ppvLD6WvA5Rhdp029IX2Mi3Mn7fp
cloud.google.com/go/translate v1.5.0/go.mod h1:29YDSYveqqpA1CQFD7NQuP49xymq17RXNaUDdc0mNu0=
cloud.google.com/go/translate v1.6.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos=
cloud.google.com/go/translate v1.7.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos=
cloud.google.com/go/vertexai v0.12.0 h1:zTadEo/CtsoyRXNx3uGCncoWAP1H2HakGqwznt+iMo8=
cloud.google.com/go/vertexai v0.12.0/go.mod h1:8u+d0TsvBfAAd2x5R6GMgbYhsLgo3J7lmP4bR8g2ig8=
cloud.google.com/go/video v1.8.0/go.mod h1:sTzKFc0bUSByE8Yoh8X0mn8bMymItVGPfTuUBUyRgxk=
cloud.google.com/go/video v1.9.0/go.mod h1:0RhNKFRF5v92f8dQt0yhaHrEuH95m068JYOvLZYnJSw=
cloud.google.com/go/video v1.12.0/go.mod h1:MLQew95eTuaNDEGriQdcYn0dTwf9oWiA4uYebxM5kdg=
@@ -661,6 +669,8 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapp
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0/go.mod h1:cSgYe11MCNYunTnRXrKiR/tHc0eoKjICUuWpNZoVCOo=
github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY=
@@ -668,6 +678,8 @@ github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0=
github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI=
@@ -734,6 +746,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
@@ -765,6 +779,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
@@ -782,8 +798,10 @@ github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmn
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
github.com/go-goquery/goquery v1.0.1 h1:kpchVA1LdOFWdRpkDPESVdlb1JQI6ixsJ5MiNUITO7U=
github.com/go-goquery/goquery v1.0.1/go.mod h1:W5s8OWbqWf6lG0LkXWBeh7U1Y/X5XTI0Br65MHF8uJk=
github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY=
github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U=
github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk=
@@ -801,8 +819,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
@@ -868,6 +886,8 @@ github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl76
github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/flatbuffers v23.5.26+incompatible h1:M9dgRyhJemaM4Sw8+66GHBu8ioaQmyPLg1b8VwK5WJg=
github.com/google/flatbuffers v23.5.26+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/generative-ai-go v0.15.1 h1:n8aQUpvhPOlGVuM2DRkJ2jvx04zpp42B778AROJa+pQ=
github.com/google/generative-ai-go v0.15.1/go.mod h1:AAucpWZjXsDKhQYWvCYuP6d0yB1kX998pJlOW1rAesw=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@@ -883,6 +903,7 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -940,6 +961,8 @@ github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3
github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w=
github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/googleapis/mcp-toolbox-sdk-go v0.2.0 h1:y242XXymvSDJ84FhDvSqpyjq4bOtRDy6yOxs7QR8etY=
github.com/googleapis/mcp-toolbox-sdk-go v0.2.0/go.mod h1:Zd5cooy5sH5ThiTwzhKtZZxTkLGbPlqDZ9c8er969Ug=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI=
@@ -989,8 +1012,8 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE=
github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI=
github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
@@ -1043,6 +1066,8 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pkoukk/tiktoken-go v0.1.6 h1:JF0TlJzhTbrI30wCvFuiw6FzP2+/bR+FIxUdgEAcUsw=
github.com/pkoukk/tiktoken-go v0.1.6/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -1092,6 +1117,8 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tmc/langchaingo v0.1.13 h1:rcpMWBIi2y3B90XxfE4Ao8dhCQPVDMaNPnN5cGB1CaA=
github.com/tmc/langchaingo v0.1.13/go.mod h1:vpQ5NOIhpzxDfTZK9B6tf2GM/MoaHewPWM5KXXGh7hg=
github.com/valkey-io/valkey-go v1.0.62 h1:oQdPlQGRyxcQWL8fnu6J3SCaQwayc/hRZifjJIaJqu0=
github.com/valkey-io/valkey-go v1.0.62/go.mod h1:bHmwjIEOrGq/ubOJfh5uMRs7Xj6mV3mQ/ZXUbmqpjqY=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
@@ -1176,6 +1203,10 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -1237,6 +1268,9 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1296,6 +1330,11 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -1345,6 +1384,10 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1426,8 +1469,14 @@ golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
@@ -1436,6 +1485,11 @@ golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -1452,6 +1506,10 @@ golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -1526,6 +1584,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1605,8 +1665,8 @@ google.golang.org/api v0.108.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/
google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60ca7iI=
google.golang.org/api v0.111.0/go.mod h1:qtFHvU9mhgTJegR31csQ+rwxyUTHOKFqCKWp1J0fdw0=
google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg=
google.golang.org/api v0.239.0 h1:2hZKUnFZEy81eugPs4e2XzIJ5SOwQg0G82bpXD65Puo=
google.golang.org/api v0.239.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50=
google.golang.org/api v0.242.0 h1:7Lnb1nfnpvbkCiZek6IXKdJ0MFuAZNAJKQfA1ws62xg=
google.golang.org/api v0.242.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -1823,6 +1883,8 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
@@ -1898,3 +1960,5 @@ rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=

View File

@@ -0,0 +1,216 @@
// agent/engine.go
package agent
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/googleapis/mcp-toolbox-sdk-go/core"
"github.com/tmc/langchaingo/llms"
"github.com/tmc/langchaingo/llms/googleai"
)
// ChatEvent is streamed to the UI via SSE.
type ChatEvent struct {
Type string `json:"type"` // user | assistant | tool_call | tool_resp | agent_error | done
Content interface{} `json:"content,omitempty"` // text or raw JSON
ToolName string `json:"toolName,omitempty"` // for tool_* events
Arguments interface{} `json:"arguments,omitempty"` // for tool_call
}
// Engine can be reused safely by many goroutines.
type Engine struct {
llm llms.LLM
langchainTools []llms.Tool // tools passed to the LLM
toolsMap map[string]*core.ToolboxTool // lookup by both hyphen and snake names
validNames []string // cached list for error messages
sysPrompt string
maxToolRuns int
}
// New builds a single Engine instance that you can share.
func New(ctx context.Context, genaiKey, toolboxURL, toolsetID string) (*Engine, error) {
llm, err := googleai.New(ctx,
googleai.WithAPIKey(genaiKey),
googleai.WithDefaultModel("gemini-2.5-pro"))
if err != nil {
return nil, fmt.Errorf("googleai: %w", err)
}
tb, err := core.NewToolboxClient(toolboxURL)
if err != nil {
return nil, fmt.Errorf("toolbox client: %w", err)
}
tools, err := tb.LoadToolset(toolsetID, ctx)
if err != nil {
return nil, fmt.Errorf("load toolset: %w", err)
}
toolsMap := make(map[string]*core.ToolboxTool, len(tools)*2)
var langTools []llms.Tool
var valid []string
for _, t := range tools {
orig := t.Name()
alias := toSnake(orig)
toolsMap[orig] = t
valid = append(valid, orig)
if alias != orig {
toolsMap[alias] = t
valid = append(valid, alias)
}
langTools = append(langTools, makeLangTool(t, alias))
}
fullPrompt := fmt.Sprintf("%s\n\nValid tools:\n- %s",
basePrompt, strings.Join(valid, "\n- "))
return &Engine{
llm: llm,
langchainTools: langTools,
toolsMap: toolsMap,
validNames: valid,
sysPrompt: fullPrompt,
maxToolRuns: 5,
}, nil
}
func (e *Engine) Run(ctx context.Context, userMsg string, sink chan<- ChatEvent) {
defer close(sink)
// seed history
history := []llms.MessageContent{
llms.TextParts(llms.ChatMessageTypeSystem, e.sysPrompt),
llms.TextParts(llms.ChatMessageTypeHuman, userMsg),
}
sink <- ChatEvent{Type: "user", Content: userMsg}
toolRuns := 0
for {
// ask the model
resp, err := e.llm.GenerateContent(ctx, history, llms.WithTools(e.langchainTools))
if err != nil {
sink <- ChatEvent{Type: "agent_error", Content: err.Error()}
return
}
choice := resp.Choices[0]
// stream assistant thought
sink <- ChatEvent{Type: "assistant", Content: choice.Content}
// if no tool calls, we're done
if len(choice.ToolCalls) == 0 {
sink <- ChatEvent{Type: "done"}
return
}
// handle every tool call synchronously
retry := false
for _, tc := range choice.ToolCalls {
if toolRuns >= e.maxToolRuns {
sink <- ChatEvent{Type: "agent_error",
Content: fmt.Sprintf("aborted: exceeded max tool runs (%d)", e.maxToolRuns)}
sink <- ChatEvent{Type: "done"}
return
}
toolRuns++
tool, ok := e.toolsMap[tc.FunctionCall.Name]
if !ok {
// hallucinated tool kept happening add correction, retry loop
msg := fmt.Sprintf("Tool %q does not exist. Valid tools: %s",
tc.FunctionCall.Name, strings.Join(e.validNames, ", "))
sink <- ChatEvent{Type: "agent_error", Content: msg}
history = append(history,
llms.TextParts(llms.ChatMessageTypeSystem, msg))
retry = true
break // leave inner loop, go back to LLM
}
// parse arguments
var args map[string]any
if err := json.Unmarshal([]byte(tc.FunctionCall.Arguments), &args); err != nil {
sink <- ChatEvent{Type: "agent_error",
Content: fmt.Sprintf("arg unmarshal: %v", err)}
sink <- ChatEvent{Type: "done"}
return
}
// announce call
sink <- ChatEvent{Type: "tool_call", ToolName: tc.FunctionCall.Name, Arguments: args}
// invoke tool
result, err := tool.Invoke(ctx, args)
if err != nil {
sink <- ChatEvent{Type: "agent_error",
Content: fmt.Sprintf("tool error: %v", err)}
sink <- ChatEvent{Type: "done"}
return
}
if result == "" || result == nil {
result = "Operation completed successfully."
}
// stream response
sink <- ChatEvent{Type: "tool_resp", ToolName: tc.FunctionCall.Name, Content: result}
// add to memory
history = append(history,
llms.MessageContent{
Role: llms.ChatMessageTypeTool,
Parts: []llms.ContentPart{
llms.ToolCallResponse{
Name: tc.FunctionCall.Name,
Content: fmt.Sprintf("%v", result),
},
},
})
}
if retry {
continue // model will be asked again with correction in history
}
// append assistant message (already streamed)
history = append(history,
llms.TextParts(llms.ChatMessageTypeAI, choice.Content))
}
}
// makeLangTool converts a Toolbox tool into a LangChain function tool.
func makeLangTool(t *core.ToolboxTool, exposedName string) llms.Tool {
schemaBytes, _ := t.InputSchema()
var paramsSchema map[string]any
_ = json.Unmarshal(schemaBytes, &paramsSchema)
return llms.Tool{
Type: "function",
Function: &llms.FunctionDefinition{
Name: exposedName,
Description: t.Description(),
Parameters: paramsSchema,
},
}
}
// toSnake replaces hyphens with underscores.
func toSnake(s string) string {
return strings.ReplaceAll(s, "-", "_")
}
const basePrompt = `
You are a helpful hotel assistant that uses tools to handle hotel searching, booking, updating, and cancellations.
Rules:
1. When the user searches for a hotel (by name, location, or price tier), call the appropriate tool.
2. Always return the hotel name, id, location, and price tier in search results.
3. When the user asks to book, update, or cancel a hotel, extract the hotel ID and use it in the tool call.
4. You may chain multiple tools in sequence, passing outputs as inputs.
5. Do NOT ask the user for confirmation; just act.
6. Call ONLY tools from list of valid tools; every other name is invalid.
`

View File

@@ -75,7 +75,7 @@ func toolsetHandler(s *Server, w http.ResponseWriter, r *http.Request) {
)
}()
toolset, ok := s.toolsets[toolsetName]
toolset, ok := s.ResourceMgr.GetToolset(toolsetName)
if !ok {
err = fmt.Errorf("toolset %q does not exist", toolsetName)
s.logger.DebugContext(ctx, err.Error())
@@ -111,7 +111,7 @@ func toolGetHandler(s *Server, w http.ResponseWriter, r *http.Request) {
metric.WithAttributes(attribute.String("toolbox.operation.status", status)),
)
}()
tool, ok := s.tools[toolName]
tool, ok := s.ResourceMgr.GetTool(toolName)
if !ok {
err = fmt.Errorf("invalid tool name: tool with name %q does not exist", toolName)
s.logger.DebugContext(ctx, err.Error())
@@ -156,7 +156,7 @@ func toolInvokeHandler(s *Server, w http.ResponseWriter, r *http.Request) {
)
}()
tool, ok := s.tools[toolName]
tool, ok := s.ResourceMgr.GetTool(toolName)
if !ok {
err = fmt.Errorf("invalid tool name: tool with name %q does not exist", toolName)
s.logger.DebugContext(ctx, err.Error())
@@ -167,7 +167,7 @@ func toolInvokeHandler(s *Server, w http.ResponseWriter, r *http.Request) {
// Tool authentication
// claimsFromAuth maps the name of the authservice to the claims retrieved from it.
claimsFromAuth := make(map[string]map[string]any)
for _, aS := range s.authServices {
for _, aS := range s.ResourceMgr.GetAuthServiceMap() {
claims, err := aS.GetClaimsFromHeader(ctx, r.Header)
if err != nil {
s.logger.DebugContext(ctx, err.Error())

View File

@@ -147,14 +147,23 @@ func setUpServer(t *testing.T, router string, tools map[string]tools.Tool, tools
t.Fatalf("unable to setup otel: %s", err)
}
instrumentation, err := CreateTelemetryInstrumentation(fakeVersionString)
instrumentation, err := telemetry.CreateTelemetryInstrumentation(fakeVersionString)
if err != nil {
t.Fatalf("unable to create custom metrics: %s", err)
}
sseManager := newSseManager(ctx)
server := Server{version: fakeVersionString, logger: testLogger, instrumentation: instrumentation, sseManager: sseManager, tools: tools, toolsets: toolsets}
resourceManager := NewResourceManager(nil, nil, tools, toolsets)
server := Server{
version: fakeVersionString,
logger: testLogger,
instrumentation: instrumentation,
sseManager: sseManager,
ResourceMgr: resourceManager,
}
var r chi.Router
switch router {
case "api":

View File

@@ -53,6 +53,10 @@ type ServerConfig struct {
TelemetryServiceName string
// Stdio indicates if Toolbox is listening via MCP stdio.
Stdio bool
// DisableReload indicates if the user has disabled dynamic reloading for Toolbox.
DisableReload bool
// UI indicates if Toolbox UI endpoints (/ui) are available
UI bool
}
type logFormat string

View File

@@ -479,12 +479,12 @@ func processMcpMessage(ctx context.Context, body []byte, s *Server, protocolVers
}
return v, res, err
default:
toolset, ok := s.toolsets[toolsetName]
toolset, ok := s.ResourceMgr.GetToolset(toolsetName)
if !ok {
err = fmt.Errorf("toolset does not exist")
return "", jsonrpc.NewError(baseMessage.Id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
}
res, err := mcp.ProcessMethod(ctx, protocolVersion, baseMessage.Id, baseMessage.Method, toolset, s.tools, body)
res, err := mcp.ProcessMethod(ctx, protocolVersion, baseMessage.Id, baseMessage.Method, toolset, s.ResourceMgr.GetToolsMap(), body)
return "", res, err
}
}

View File

@@ -693,14 +693,22 @@ func TestStdioSession(t *testing.T) {
}
}()
instrumentation, err := CreateTelemetryInstrumentation(fakeVersionString)
instrumentation, err := telemetry.CreateTelemetryInstrumentation(fakeVersionString)
if err != nil {
t.Fatalf("unable to create custom metrics: %s", err)
}
sseManager := newSseManager(ctx)
server := &Server{version: fakeVersionString, logger: testLogger, instrumentation: instrumentation, sseManager: sseManager, tools: toolsMap, toolsets: toolsets}
resourceManager := NewResourceManager(nil, nil, toolsMap, toolsets)
server := &Server{
version: fakeVersionString,
logger: testLogger,
instrumentation: instrumentation,
sseManager: sseManager,
ResourceMgr: resourceManager,
}
in := bufio.NewReader(pr)
stdioSession := NewStdioSession(server, in, pw)

View File

@@ -21,6 +21,7 @@ import (
"net"
"net/http"
"strconv"
"sync"
"time"
"github.com/go-chi/chi/v5"
@@ -29,6 +30,7 @@ import (
"github.com/googleapis/genai-toolbox/internal/auth"
"github.com/googleapis/genai-toolbox/internal/log"
"github.com/googleapis/genai-toolbox/internal/sources"
"github.com/googleapis/genai-toolbox/internal/telemetry"
"github.com/googleapis/genai-toolbox/internal/tools"
"github.com/googleapis/genai-toolbox/internal/util"
"go.opentelemetry.io/otel/attribute"
@@ -42,26 +44,225 @@ type Server struct {
listener net.Listener
root chi.Router
logger log.Logger
instrumentation *Instrumentation
instrumentation *telemetry.Instrumentation
sseManager *sseManager
ResourceMgr *ResourceManager
}
// ResourceManager contains available resources for the server. Should be initialized with NewResourceManager().
type ResourceManager struct {
mu sync.RWMutex
sources map[string]sources.Source
authServices map[string]auth.AuthService
tools map[string]tools.Tool
toolsets map[string]tools.Toolset
}
// NewServer returns a Server object based on provided Config.
func NewServer(ctx context.Context, cfg ServerConfig, l log.Logger) (*Server, error) {
instrumentation, err := CreateTelemetryInstrumentation(cfg.Version)
func NewResourceManager(
sourcesMap map[string]sources.Source,
authServicesMap map[string]auth.AuthService,
toolsMap map[string]tools.Tool, toolsetsMap map[string]tools.Toolset,
) *ResourceManager {
resourceMgr := &ResourceManager{
mu: sync.RWMutex{},
sources: sourcesMap,
authServices: authServicesMap,
tools: toolsMap,
toolsets: toolsetsMap,
}
return resourceMgr
}
func (r *ResourceManager) GetSource(sourceName string) (sources.Source, bool) {
r.mu.RLock()
defer r.mu.RUnlock()
source, ok := r.sources[sourceName]
return source, ok
}
func (r *ResourceManager) GetAuthService(authServiceName string) (auth.AuthService, bool) {
r.mu.RLock()
defer r.mu.RUnlock()
authService, ok := r.authServices[authServiceName]
return authService, ok
}
func (r *ResourceManager) GetTool(toolName string) (tools.Tool, bool) {
r.mu.RLock()
defer r.mu.RUnlock()
tool, ok := r.tools[toolName]
return tool, ok
}
func (r *ResourceManager) GetToolset(toolsetName string) (tools.Toolset, bool) {
r.mu.RLock()
defer r.mu.RUnlock()
toolset, ok := r.toolsets[toolsetName]
return toolset, ok
}
func (r *ResourceManager) SetResources(sourcesMap map[string]sources.Source, authServicesMap map[string]auth.AuthService, toolsMap map[string]tools.Tool, toolsetsMap map[string]tools.Toolset) {
r.mu.Lock()
defer r.mu.Unlock()
r.sources = sourcesMap
r.authServices = authServicesMap
r.tools = toolsMap
r.toolsets = toolsetsMap
}
func (r *ResourceManager) GetAuthServiceMap() map[string]auth.AuthService {
r.mu.RLock()
defer r.mu.RUnlock()
return r.authServices
}
func (r *ResourceManager) GetToolsMap() map[string]tools.Tool {
r.mu.RLock()
defer r.mu.RUnlock()
return r.tools
}
func InitializeConfigs(ctx context.Context, cfg ServerConfig) (
map[string]sources.Source,
map[string]auth.AuthService,
map[string]tools.Tool,
map[string]tools.Toolset,
error,
) {
ctx = util.WithUserAgent(ctx, cfg.Version)
instrumentation, err := util.InstrumentationFromContext(ctx)
if err != nil {
return nil, fmt.Errorf("unable to create telemetry instrumentation: %w", err)
panic(err)
}
l, err := util.LoggerFromContext(ctx)
if err != nil {
panic(err)
}
// initialize and validate the sources from configs
sourcesMap := make(map[string]sources.Source)
for name, sc := range cfg.SourceConfigs {
s, err := func() (sources.Source, error) {
childCtx, span := instrumentation.Tracer.Start(
ctx,
"toolbox/server/source/init",
trace.WithAttributes(attribute.String("source_kind", sc.SourceConfigKind())),
trace.WithAttributes(attribute.String("source_name", name)),
)
defer span.End()
s, err := sc.Initialize(childCtx, instrumentation.Tracer)
if err != nil {
return nil, fmt.Errorf("unable to initialize source %q: %w", name, err)
}
return s, nil
}()
if err != nil {
return nil, nil, nil, nil, err
}
sourcesMap[name] = s
}
l.InfoContext(ctx, fmt.Sprintf("Initialized %d sources.", len(sourcesMap)))
// initialize and validate the auth services from configs
authServicesMap := make(map[string]auth.AuthService)
for name, sc := range cfg.AuthServiceConfigs {
a, err := func() (auth.AuthService, error) {
_, span := instrumentation.Tracer.Start(
ctx,
"toolbox/server/auth/init",
trace.WithAttributes(attribute.String("auth_kind", sc.AuthServiceConfigKind())),
trace.WithAttributes(attribute.String("auth_name", name)),
)
defer span.End()
a, err := sc.Initialize()
if err != nil {
return nil, fmt.Errorf("unable to initialize auth service %q: %w", name, err)
}
return a, nil
}()
if err != nil {
return nil, nil, nil, nil, err
}
authServicesMap[name] = a
}
l.InfoContext(ctx, fmt.Sprintf("Initialized %d authServices.", len(authServicesMap)))
// initialize and validate the tools from configs
toolsMap := make(map[string]tools.Tool)
for name, tc := range cfg.ToolConfigs {
t, err := func() (tools.Tool, error) {
_, span := instrumentation.Tracer.Start(
ctx,
"toolbox/server/tool/init",
trace.WithAttributes(attribute.String("tool_kind", tc.ToolConfigKind())),
trace.WithAttributes(attribute.String("tool_name", name)),
)
defer span.End()
t, err := tc.Initialize(sourcesMap)
if err != nil {
return nil, fmt.Errorf("unable to initialize tool %q: %w", name, err)
}
return t, nil
}()
if err != nil {
return nil, nil, nil, nil, err
}
toolsMap[name] = t
}
l.InfoContext(ctx, fmt.Sprintf("Initialized %d tools.", len(toolsMap)))
// create a default toolset that contains all tools
allToolNames := make([]string, 0, len(toolsMap))
for name := range toolsMap {
allToolNames = append(allToolNames, name)
}
if cfg.ToolsetConfigs == nil {
cfg.ToolsetConfigs = make(ToolsetConfigs)
}
cfg.ToolsetConfigs[""] = tools.ToolsetConfig{Name: "", ToolNames: allToolNames}
// initialize and validate the toolsets from configs
toolsetsMap := make(map[string]tools.Toolset)
for name, tc := range cfg.ToolsetConfigs {
t, err := func() (tools.Toolset, error) {
_, span := instrumentation.Tracer.Start(
ctx,
"toolbox/server/toolset/init",
trace.WithAttributes(attribute.String("toolset_name", name)),
)
defer span.End()
t, err := tc.Initialize(cfg.Version, toolsMap)
if err != nil {
return tools.Toolset{}, fmt.Errorf("unable to initialize toolset %q: %w", name, err)
}
return t, err
}()
if err != nil {
return nil, nil, nil, nil, err
}
toolsetsMap[name] = t
}
l.InfoContext(ctx, fmt.Sprintf("Initialized %d toolsets.", len(toolsetsMap)))
return sourcesMap, authServicesMap, toolsMap, toolsetsMap, nil
}
// NewServer returns a Server object based on provided Config.
func NewServer(ctx context.Context, cfg ServerConfig) (*Server, error) {
instrumentation, err := util.InstrumentationFromContext(ctx)
if err != nil {
return nil, err
}
ctx, span := instrumentation.Tracer.Start(ctx, "toolbox/server/init")
defer span.End()
ctx = util.WithUserAgent(ctx, cfg.Version)
l, err := util.LoggerFromContext(ctx)
if err != nil {
return nil, err
}
// set up http serving
r := chi.NewRouter()
@@ -97,116 +298,18 @@ func NewServer(ctx context.Context, cfg ServerConfig, l log.Logger) (*Server, er
httpLogger := httplog.NewLogger("httplog", httpOpts)
r.Use(httplog.RequestLogger(httpLogger))
// initialize and validate the sources from configs
sourcesMap := make(map[string]sources.Source)
for name, sc := range cfg.SourceConfigs {
s, err := func() (sources.Source, error) {
childCtx, span := instrumentation.Tracer.Start(
ctx,
"toolbox/server/source/init",
trace.WithAttributes(attribute.String("source_kind", sc.SourceConfigKind())),
trace.WithAttributes(attribute.String("source_name", name)),
)
defer span.End()
s, err := sc.Initialize(childCtx, instrumentation.Tracer)
if err != nil {
return nil, fmt.Errorf("unable to initialize source %q: %w", name, err)
}
return s, nil
}()
if err != nil {
return nil, err
}
sourcesMap[name] = s
sourcesMap, authServicesMap, toolsMap, toolsetsMap, err := InitializeConfigs(ctx, cfg)
if err != nil {
return nil, fmt.Errorf("unable to initialize configs: %w", err)
}
l.InfoContext(ctx, fmt.Sprintf("Initialized %d sources.", len(sourcesMap)))
// initialize and validate the auth services from configs
authServicesMap := make(map[string]auth.AuthService)
for name, sc := range cfg.AuthServiceConfigs {
a, err := func() (auth.AuthService, error) {
_, span := instrumentation.Tracer.Start(
ctx,
"toolbox/server/auth/init",
trace.WithAttributes(attribute.String("auth_kind", sc.AuthServiceConfigKind())),
trace.WithAttributes(attribute.String("auth_name", name)),
)
defer span.End()
a, err := sc.Initialize()
if err != nil {
return nil, fmt.Errorf("unable to initialize auth service %q: %w", name, err)
}
return a, nil
}()
if err != nil {
return nil, err
}
authServicesMap[name] = a
}
l.InfoContext(ctx, fmt.Sprintf("Initialized %d authServices.", len(authServicesMap)))
// initialize and validate the tools from configs
toolsMap := make(map[string]tools.Tool)
for name, tc := range cfg.ToolConfigs {
t, err := func() (tools.Tool, error) {
_, span := instrumentation.Tracer.Start(
ctx,
"toolbox/server/tool/init",
trace.WithAttributes(attribute.String("tool_kind", tc.ToolConfigKind())),
trace.WithAttributes(attribute.String("tool_name", name)),
)
defer span.End()
t, err := tc.Initialize(sourcesMap)
if err != nil {
return nil, fmt.Errorf("unable to initialize tool %q: %w", name, err)
}
return t, nil
}()
if err != nil {
return nil, err
}
toolsMap[name] = t
}
l.InfoContext(ctx, fmt.Sprintf("Initialized %d tools.", len(toolsMap)))
// create a default toolset that contains all tools
allToolNames := make([]string, 0, len(toolsMap))
for name := range toolsMap {
allToolNames = append(allToolNames, name)
}
if cfg.ToolsetConfigs == nil {
cfg.ToolsetConfigs = make(ToolsetConfigs)
}
cfg.ToolsetConfigs[""] = tools.ToolsetConfig{Name: "", ToolNames: allToolNames}
// initialize and validate the toolsets from configs
toolsetsMap := make(map[string]tools.Toolset)
for name, tc := range cfg.ToolsetConfigs {
t, err := func() (tools.Toolset, error) {
_, span := instrumentation.Tracer.Start(
ctx,
"toolbox/server/toolset/init",
trace.WithAttributes(attribute.String("toolset_name", name)),
)
defer span.End()
t, err := tc.Initialize(cfg.Version, toolsMap)
if err != nil {
return tools.Toolset{}, fmt.Errorf("unable to initialize toolset %q: %w", name, err)
}
return t, err
}()
if err != nil {
return nil, err
}
toolsetsMap[name] = t
}
l.InfoContext(ctx, fmt.Sprintf("Initialized %d toolsets.", len(toolsetsMap)))
addr := net.JoinHostPort(cfg.Address, strconv.Itoa(cfg.Port))
srv := &http.Server{Addr: addr, Handler: r}
sseManager := newSseManager(ctx)
resourceManager := NewResourceManager(sourcesMap, authServicesMap, toolsMap, toolsetsMap)
s := &Server{
version: cfg.Version,
srv: srv,
@@ -214,11 +317,7 @@ func NewServer(ctx context.Context, cfg ServerConfig, l log.Logger) (*Server, er
logger: l,
instrumentation: instrumentation,
sseManager: sseManager,
sources: sourcesMap,
authServices: authServicesMap,
tools: toolsMap,
toolsets: toolsetsMap,
ResourceMgr: resourceManager,
}
// control plane
apiR, err := apiRouter(s)
@@ -231,6 +330,13 @@ func NewServer(ctx context.Context, cfg ServerConfig, l log.Logger) (*Server, er
return nil, err
}
r.Mount("/mcp", mcpR)
if cfg.UI {
webR, err := webRouter()
if err != nil {
return nil, err
}
r.Mount("/ui", webR)
}
// default endpoint for validating server is running
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("🧰 Hello, World! 🧰"))

View File

@@ -23,9 +23,16 @@ import (
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/googleapis/genai-toolbox/internal/auth"
"github.com/googleapis/genai-toolbox/internal/log"
"github.com/googleapis/genai-toolbox/internal/server"
"github.com/googleapis/genai-toolbox/internal/sources"
"github.com/googleapis/genai-toolbox/internal/sources/alloydbpg"
"github.com/googleapis/genai-toolbox/internal/telemetry"
"github.com/googleapis/genai-toolbox/internal/testutils"
"github.com/googleapis/genai-toolbox/internal/tools"
"github.com/googleapis/genai-toolbox/internal/util"
)
func TestServe(t *testing.T) {
@@ -54,8 +61,16 @@ func TestServe(t *testing.T) {
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
ctx = util.WithLogger(ctx, testLogger)
s, err := server.NewServer(ctx, cfg, testLogger)
instrumentation, err := telemetry.CreateTelemetryInstrumentation(cfg.Version)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
ctx = util.WithInstrumentation(ctx, instrumentation)
s, err := server.NewServer(ctx, cfg)
if err != nil {
t.Fatalf("unable to initialize server: %v", err)
}
@@ -93,3 +108,67 @@ func TestServe(t *testing.T) {
t.Fatalf("version missing from output: %q", got)
}
}
func TestUpdateServer(t *testing.T) {
ctx, err := testutils.ContextWithNewLogger()
if err != nil {
t.Fatalf("error setting up logger: %s", err)
}
addr, port := "127.0.0.1", 5000
cfg := server.ServerConfig{
Version: "0.0.0",
Address: addr,
Port: port,
}
instrumentation, err := telemetry.CreateTelemetryInstrumentation(cfg.Version)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
ctx = util.WithInstrumentation(ctx, instrumentation)
s, err := server.NewServer(ctx, cfg)
if err != nil {
t.Fatalf("error setting up server: %s", err)
}
newSources := map[string]sources.Source{
"example-source": &alloydbpg.Source{
Name: "example-alloydb-source",
Kind: "alloydb-postgres",
},
}
newAuth := map[string]auth.AuthService{"example-auth": nil}
newTools := map[string]tools.Tool{"example-tool": nil}
newToolsets := map[string]tools.Toolset{
"example-toolset": {
Name: "example-toolset", Tools: []*tools.Tool{},
},
}
s.ResourceMgr.SetResources(newSources, newAuth, newTools, newToolsets)
if err != nil {
t.Errorf("error updating server: %s", err)
}
gotSource, _ := s.ResourceMgr.GetSource("example-source")
if diff := cmp.Diff(gotSource, newSources["example-source"]); diff != "" {
t.Errorf("error updating server, sources (-want +got):\n%s", diff)
}
gotAuthService, _ := s.ResourceMgr.GetAuthService("example-auth")
if diff := cmp.Diff(gotAuthService, newAuth["example-auth"]); diff != "" {
t.Errorf("error updating server, authServices (-want +got):\n%s", diff)
}
gotTool, _ := s.ResourceMgr.GetTool("example-tool")
if diff := cmp.Diff(gotTool, newTools["example-tool"]); diff != "" {
t.Errorf("error updating server, tools (-want +got):\n%s", diff)
}
gotToolset, _ := s.ResourceMgr.GetToolset("example-toolset")
if diff := cmp.Diff(gotToolset, newToolsets["example-toolset"]); diff != "" {
t.Errorf("error updating server, toolset (-want +got):\n%s", diff)
}
}

View File

@@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>Toolbox Chat</title>
<link rel="stylesheet" href="/ui/css/style.css" />
<style>
#chat-log { padding: 1rem; max-height: 80vh; overflow-y: auto; }
.user { text-align: right; color: #007bff; margin: .5rem 0; }
.assistant { text-align: left; color: #333; margin: .5rem 0; }
.tool_call { font-style: italic; color: #555; }
.tool_resp { font-family: monospace; white-space: pre; background:#fafafa; padding:.25rem }
</style>
</head>
<body>
<div id="navbar-container" data-active-nav=""></div>
<div id="chat-log"></div>
<form id="chat-form" style="padding:1rem;display:flex;gap:.5rem">
<input id="msg" style="flex:1" placeholder="Ask me anything…" autocomplete="off" />
<button type="submit">Send</button>
</form>
<script src="/ui/js/navbar.js"></script>
<script type="module" src="/ui/js/agent.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () =>
renderNavbar('navbar-container', '')
);
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

View File

@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html>
<head>
<title>OAuth ID Token Generator</title>
<script src="https://accounts.google.com/gsi/client" async defer></script>
</head>
<body>
<h1>Get Google ID Token</h1>
<label for="clientIdInput">OAuth Client ID:</label>
<input type="text" id="clientIdInput" placeholder="Enter Client ID (e.g., ....apps.googleusercontent.com)">
<button onclick="startSignIn()">Get ID Token</button>
<div id="gisContainer" style="margin-top: 20px;">
</div>
<h3>ID Token:</h3>
<textarea id="idTokenResult" rows="15" cols="80" readonly></textarea>
<h3>ID Token Claims (Decoded):</h3>
<pre id="idTokenClaims"></pre>
<script src="/ui/js/auth.js"></script>
</body>
</html>

View File

@@ -0,0 +1,691 @@
:root {
--toolbox-blue: #4285f4;
--text-primary-gray: #444444;
--text-secondary-gray: #6e6e6e;
--button-primary: var(--toolbox-blue);
--button-secondary: #616161;
}
body {
display: flex;
height: 100vh;
margin: 0;
font-family: 'Trebuchet MS';
background-color: #f8f9fa;
box-sizing: border-box;
}
*, *:before, *:after {
box-sizing: inherit;
}
#navbar-container {
flex: 0 0 250px;
height: 100%;
position: relative;
z-index: 10;
}
#main-content-container {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
overflow-x: hidden;
}
.left-nav {
background-color: #fff;
box-shadow: 4px 0px 12px rgba(0, 0, 0, 0.15);
display: flex;
flex-direction: column;
padding: 15px;
align-items: center;
width: 100%;
height: 100%;
z-index: 3;
ul {
font-family: 'Verdana';
list-style: none;
padding: 0;
margin: 0;
width: 100%;
li {
margin-bottom: 5px;
a {
display: flex;
align-items: center;
padding: 12px;
text-decoration: none;
color: #333;
border-radius: 0;
&:hover {
background-color: #e9e9e9;
border-radius: 35px;
}
&.active {
background-color: #d0d0d0;
font-weight: bold;
border-radius: 35px;
}
}
}
}
}
.second-nav {
flex: 0 0 250px;
background-color: #fff;
box-shadow: 4px 0px 12px rgba(0, 0, 0, 0.15);
z-index: 2;
display: flex;
flex-direction: column;
padding: 15px;
align-items: center;
position: relative;
}
.nav-logo {
width: 90%;
margin-bottom: 20px;
flex-shrink: 0;
img {
max-width: 100%;
height: auto;
display: block;
}
}
.main-content-area {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
overflow-x: hidden;
}
.top-bar {
background-color: #fff;
padding: 30px 30px;
display: flex;
justify-content: flex-end;
align-items: center;
border-bottom: 1px solid #eee;
}
.content {
padding: 20px;
flex-grow: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.btn {
display: flex;
align-items: center;
justify-content: center;
padding: 10px 20px;
color: white;
border: none;
border-radius: 30px;
font: inherit;
font-size: 1em;
font-weight: bolder;
cursor: pointer;
&:hover {
opacity: 0.8;
}
}
.btn--run {
background-color: var(--button-primary);
}
.btn--editHeaders {
background-color: var(--button-secondary)
}
.btn--saveHeaders {
background-color: var(--button-primary)
}
.btn--closeHeaders {
background-color: var(--button-secondary)
}
.btn--setup-gis {
background-color: var(--button-secondary)
}
.tool-button {
display: flex;
align-items: center;
padding: 12px;
text-decoration: none;
color: #333;
background-color: transparent;
border: none;
border-radius: 0;
width: 100%;
text-align: left;
cursor: pointer;
font-family: inherit;
font-size: inherit;
transition: background-color 0.1s ease-in-out, border-radius 0.1s ease-in-out;
&:hover {
background-color: #e9e9e9;
border-radius: 35px;
}
&:focus {
outline: none;
box-shadow: 0 0 0 2px rgba(208, 208, 208, 0.5);
}
&.active {
background-color: #d0d0d0;
font-weight: bold;
border-radius: 35px;
&:hover {
background-color: #d0d0d0;
}
}
}
#secondary-panel-content {
flex: 1;
overflow-y: auto;
width: 100%;
min-height: 0;
ul {
list-style: none;
padding: 0;
margin: 0;
width: 100%;
}
}
.tool-details-grid {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 20px;
margin: 0 0 20px 0;
align-items: start;
flex-shrink: 0;
}
.tool-info {
display: flex;
flex-direction: column;
gap: 15px;
}
.tool-execution-area {
display: flex;
flex-direction: column;
gap: 12px;
}
.tool-params {
background-color: #ffffff;
padding: 15px;
border-radius: 4px;
border: 1px solid #ddd;
h5 {
margin-bottom: 0;
}
}
.tool-box {
background-color: #ffffff;
padding: 15px;
border-radius: 4px;
border: 1px solid #eee;
h5 {
color: var(--toolbox-blue);
margin-top: 0;
font-weight: bold;
}
}
.params-header {
display: flex;
justify-content: flex-end;
margin-bottom: 8px;
padding-right: 6px;
font-weight: bold;
font-size: 0.9em;
color: var(--text-secondary-gray);
}
.params-disclaimer {
font-style: italic;
color: var(--text-secondary-gray);
font-size: 0.8em;
margin-bottom: 10px;
width: 100%;
word-wrap: break-word;
}
.param-item {
margin-bottom: 12px;
label {
display: block;
margin-bottom: 4px;
font-family: inherit;
}
&.disabled-param {
> label {
color: #888;
text-decoration: line-through;
}
.param-input-element {
background-color: #f5f5f5;
opacity: 0.6;
}
}
input[type="text"],
input[type="number"],
select,
textarea {
width: calc(100% - 12px);
padding: 6px;
border: 1px solid #ccc;
border-radius: 4px;
font-family: inherit;
}
input[type="checkbox"].param-input-element {
width: auto;
padding: 0;
border: initial;
border-radius: initial;
vertical-align: middle;
margin-right: 4px;
accent-color: var(--toolbox-blue);
flex-grow: 0;
}
}
.input-checkbox-wrapper {
display: flex;
align-items: center;
gap: 10px;
}
.param-input-element-container {
flex-grow: 1;
}
.param-input-element {
box-sizing: border-box;
}
.include-param-container {
display: flex;
align-items: center;
white-space: nowrap;
input[type="checkbox"] {
width: auto;
padding: 0;
border: initial;
border-radius: initial;
vertical-align: middle;
margin-right: 0;
accent-color: var(--toolbox-blue);
}
}
.include-param-container input[type="checkbox"] {
width: auto;
padding: 0;
border: initial;
border-radius: initial;
vertical-align: middle;
margin: 0;
accent-color: var(--toolbox-blue);
}
.checkbox-bool-label {
margin-left: 5px;
font-style: italic;
color: var(--text-primary-gray);
}
.checkbox-bool-label.disabled {
color: #aaa;
cursor: not-allowed;
}
.param-label-extras {
font-style: italic;
font-weight: lighter;
color: var(--text-secondary-gray);
}
.auth-param-input {
background-color: #e0e0e0;
cursor: not-allowed;
}
.run-button-container {
display: flex;
justify-content: flex-end;
gap: 20px;
}
.header-modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0,0,0,0.4);
li {
margin-bottom: 10px;
}
.header-modal-content {
background-color: #fefefe;
margin: 10% auto;
padding: 20px;
border: 1px solid #888;
width: 80%;
max-width: 50%;
border-radius: 8px;
display: flex;
flex-direction: column;
gap: 15px;
align-items: center;
h5 {
margin-top: 0;
font-size: 1.2em;
}
.headers-textarea {
width: calc(100% - 16px);
padding: 8px;
font-family: monospace;
border: 1px solid #ccc;
border-radius: 4px;
min-height: 150px;
}
.header-modal-actions {
display: flex;
justify-content: center;
gap: 30px;
width: 100%;
}
.auth-token-details {
width: 100%;
max-width: calc(100% - 16px);
margin-left: 8px;
margin-right: 8px;
summary {
cursor: pointer;
text-align: left;
padding: 5px 0;
}
.auth-token-content {
padding: 10px;
border: 1px solid #eee;
margin-top: 5px;
background-color: #f9f9f9;
text-align: left;
max-width: 100%;
overflow-wrap: break-word;
.auth-tab-group {
display: flex;
border-bottom: 1px solid #ccc;
margin-bottom: 10px;
}
.auth-tab-picker {
padding: 8px 12px;
cursor: pointer;
border: 1px solid transparent;
border-bottom: 1px solid transparent;
margin-bottom: -1px;
background-color: #f0f0f0;
&.active {
background-color: #fff;
border-color: #ccc;
border-bottom-color: #fff;
font-weight: bold;
}
}
.auth-tab-content {
display: none;
overflow-wrap: break-word;
word-wrap: break-word;
max-width: 100%;
&.active {
display: block;
}
pre {
white-space: pre-wrap;
word-wrap: break-word;
overflow-x: auto;
background-color: #f5f5f5;
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
max-width: 100%;
code {
display: block;
word-wrap: break-word;
color: inherit;
}
}
}
}
}
}
}
.auth-method-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background-color: #f8f9fa; /* Light grey background */
border: 1px solid #e0e0e0; /* Light border */
border-radius: 4px;
margin-bottom: 8px;
}
.auth-method-label {
font-weight: 500;
color: #3c4043; /* Dark grey text */
}
.auth-helper-section {
border: 1px solid #e0e0e0;
padding: 16px;
border-radius: 8px;
margin-top: 20px;
background-color: #f8f9fa;
width: 60%;
}
.auth-helper-title {
margin-top: 0;
margin-bottom: 16px;
color: var(--text-primary-gray);
font-size: 1.15em;
font-weight: 500;
}
.auth-method-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.auth-method-details {
padding: 16px;
border: 1px solid #e0e0e0;
border-radius: 4px;
margin-bottom: 16px;
background-color: #fff;
}
/* Wrapper for input rows and action buttons */
.auth-controls {
display: flex;
flex-direction: column;
gap: 16px;
}
/* Row containing a label and an input field */
.auth-input-row {
display: flex;
flex-direction: column;
gap: 6px;
}
.auth-input-row label {
font-size: 14px;
color: #3c4043;
margin-bottom: 4px;
}
/* Text input field style */
.auth-input {
padding: 8px 10px;
border: 1px solid #bdc1c6; /* Grey border */
border-radius: 4px;
font-size: 14px;
width: 100%;
box-sizing: border-box;
}
.auth-input:focus {
outline: none;
border-color: #1a73e8; /* Blue border on focus */
box-shadow: 0 0 0 1px #1a73e8;
}
/* Container for action buttons within the details section */
.auth-method-actions {
display: flex;
align-items: center;
gap: 12px;
}
.tool-response {
margin: 20px 0 0 0;
textarea {
width: 100%;
min-height: 150px;
padding: 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-family: monospace;
}
}
.search-container {
display: flex;
width: 100%;
margin-bottom: 15px;
#toolset-search-input {
flex-grow: 1;
padding: 10px 12px;
border: 1px solid #ccc;
border-radius: 20px 0 0 20px;
border-right: none;
font-family: inherit;
font-size: 0.9em;
color: var(--text-primary-gray);
&:focus {
outline: none;
border-color: var(--toolbox-blue);
box-shadow: 0 0 0 2px rgba(66, 133, 244, 0.3);
}
&::placeholder {
color: var(--text-secondary-gray);
}
}
#toolset-search-button {
padding: 10px 15px;
border: 1px solid var(--button-primary);
background-color: var(--button-primary);
color: white;
border-radius: 0 20px 20px 0;
cursor: pointer;
font-family: inherit;
font-size: 0.9em;
font-weight: bold;
transition: opacity 0.2s ease-in-out;
flex-shrink: 0;
&:hover {
opacity: 0.8;
}
&:focus {
outline: none;
box-shadow: 0 0 0 2px rgba(66, 133, 244, 0.3);
}
}
}
.toggle-details-tab {
background-color: transparent;
color: var(--toolbox-blue);
border: none;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 1em;
font-weight: bold;
&:hover {
opacity: 0.8;
}
}

View File

@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Toolbox UI</title>
<link rel="stylesheet" href="/ui/css/style.css">
</head>
<body>
<div id="navbar-container" data-active-nav=""></div>
<div id="main-content-container"></div>
<script src="/ui/js/navbar.js"></script>
<script src="/ui/js/mainContent.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const navbarContainer = document.getElementById('navbar-container');
const activeNav = navbarContainer.getAttribute('data-active-nav');
renderNavbar('navbar-container', activeNav);
renderMainContent('main-content-container', '')
});
</script>
</body>
</html>

View File

@@ -0,0 +1,62 @@
const log = document.getElementById("chat-log");
const form = document.getElementById("chat-form");
const input = document.getElementById("msg");
let es; // EventSource
form.onsubmit = async (e) => {
e.preventDefault();
const txt = input.value.trim();
if (!txt) return;
append("user", txt);
// start conversation
const res = await fetch("/ui/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: txt })
});
const { id } = await res.json();
// subscribe to SSE
es?.close();
es = new EventSource(`/ui/chat/${id}/events`);
// ---------- connectionlevel errors ----------
es.addEventListener("error", (ev) => {
if (es.readyState === EventSource.CLOSED) return;
console.error("EventSource connection error", ev);
append("error", { content: "lost connection to server" });
});
// ---------- serversent events we expect ----------
["assistant", "tool_call", "tool_resp", "agent_error", "done"].forEach(type => {
es.addEventListener(type, (ev) => {
if (type === "done") {
append("assistant", { content: "✓ conversation finished" });
es.close();
return;
}
const data = JSON.parse(ev.data);
append(type, data);
});
});
input.value = "";
};
function append(type, payload) {
const div = document.createElement("div");
div.className = type;
switch (type) {
case "tool_call":
div.textContent = `${payload.toolName}(${JSON.stringify(payload.arguments)})`;
break;
case "tool_resp":
div.textContent = `${payload.toolName}${JSON.stringify(payload.content)}`;
break;
default:
div.textContent = (payload.content ?? payload);
}
log.appendChild(div);
log.scrollTop = log.scrollHeight; // autoscroll
}

View File

@@ -0,0 +1,160 @@
/**
* Handles the credential response from the Google Sign-In library.
* @param {!CredentialResponse} response The credential response object from GIS.
* @param {string} toolId The ID of the tool.
* @param {string} authProfileName The name of the authentication profile.
*/
function handleCredentialResponse(response, toolId, authProfileName) {
console.log("handleCredentialResponse called with:", { response, toolId, authProfileName });
const headersTextarea = document.getElementById(`headers-textarea-${toolId}`);
if (!headersTextarea) {
console.error('Headers textarea not found for toolId:', toolId);
return;
}
const uniqueIdBase = `${toolId}-${authProfileName}`;
const setupGisBtn = document.querySelector(`#google-auth-details-${uniqueIdBase} .setup-gis-btn`);
const gisContainer = document.getElementById(`gisContainer-${uniqueIdBase}`);
if (response.credential) {
const idToken = response.credential;
console.log("ID Token:", idToken);
try {
let currentHeaders = {};
if (headersTextarea.value) {
currentHeaders = JSON.parse(headersTextarea.value);
}
const headerKey = `${authProfileName}_token`;
currentHeaders[headerKey] = `${idToken}`;
headersTextarea.value = JSON.stringify(currentHeaders, null, 2);
// alert(`Header '${headerKey}' updated.`);
if (gisContainer) gisContainer.style.display = 'none';
if (setupGisBtn) setupGisBtn.style.display = '';
} catch (e) {
alert('Headers are not valid JSON. Please correct and try again.');
console.error("Header JSON parse error:", e);
}
} else {
console.error("Error: No credential in response", response);
alert('Error: No ID Token received. Check console for details.');
if (gisContainer) gisContainer.style.display = 'none';
if (setupGisBtn) setupGisBtn.style.display = '';
}
}
/**
* Renders the Google Sign-In button using the GIS library.
* @param {string} toolId The ID of the tool.
* @param {string} clientId The Google OAuth Client ID.
* @param {string} authProfileName The name of the authentication profile.
*/
function renderGoogleSignInButton(toolId, clientId, authProfileName) {
const uniqueIdBase = `${toolId}-${authProfileName}`;
const gisContainerId = `gisContainer-${uniqueIdBase}`;
const gisContainer = document.getElementById(gisContainerId);
const setupGisBtn = document.querySelector(`#google-auth-details-${uniqueIdBase} .setup-gis-btn`);
if (!gisContainer) {
console.error('GIS container not found:', gisContainerId);
return;
}
if (!clientId) {
alert('Please enter an OAuth Client ID first.');
return;
}
// Hide the setup button and show the container for the GIS button
if (setupGisBtn) setupGisBtn.style.display = 'none';
gisContainer.innerHTML = ''; // Clear previous button
gisContainer.style.display = 'flex'; // Make it visible
console.log(window.google, window.googleaccounts, window.google.accounts.id)
if (window.google && window.google.accounts && window.google.accounts.id) {
try {
console.log("attempting handle response")
const handleResponse = (response) => handleCredentialResponse(response, toolId, authProfileName);
window.google.accounts.id.initialize({
client_id: clientId,
callback: handleResponse,
auto_select: false
});
console.log("initialized account")
window.google.accounts.id.renderButton(
gisContainer,
{ theme: "outline", size: "large", text: "signin_with" }
);
} catch (error) {
console.error("Error initializing Google Sign-In:", error);
alert("Error initializing Google Sign-In. Check the Client ID and browser console.");
gisContainer.innerHTML = '<p style="color: red;">Error loading Sign-In button.</p>';
if (setupGisBtn) setupGisBtn.style.display = '';
}
} else {
console.error("GIS library not fully loaded yet.");
alert("Google Identity Services library not ready. Please try again in a moment.");
gisContainer.innerHTML = '<p style="color: red;">GIS library not ready.</p>';
if (setupGisBtn) setupGisBtn.style.display = '';
}
}
// creates the Google Auth method dropdown
export function createGoogleAuthMethodItem(toolId, authProfileName) {
const item = document.createElement('div');
item.className = 'auth-method-item';
const uniqueIdBase = `${toolId}-${authProfileName}`;
item.innerHTML = `
<div class="auth-method-header">
<span class="auth-method-label">Google ID Token (${authProfileName})</span>
<button class="toggle-details-tab">Setup</button>
</div>
<div class="auth-method-details" id="google-auth-details-${uniqueIdBase}" style="display: none;">
<div class="auth-controls">
<div class="auth-input-row">
<label for="clientIdInput-${uniqueIdBase}">OAuth Client ID:</label>
<input type="text" id="clientIdInput-${uniqueIdBase}" placeholder="Enter Client ID" class="auth-input">
</div>
<div class="auth-method-actions">
<button class="btn btn--setup-gis">Add Token</button>
<div id="gisContainer-${uniqueIdBase}" class="auth-interactive-element gis-container" style="display: none;"></div>
</div>
</div>
</div>
`;
const toggleBtn = item.querySelector('.toggle-details-tab');
const detailsDiv = item.querySelector(`#google-auth-details-${uniqueIdBase}`);
const setupGisBtn = item.querySelector('.btn--setup-gis');
const clientIdInput = item.querySelector(`#clientIdInput-${uniqueIdBase}`);
const gisContainer = item.querySelector(`#gisContainer-${uniqueIdBase}`);
toggleBtn.addEventListener('click', () => {
const isVisible = detailsDiv.style.display === 'flex';
detailsDiv.style.display = isVisible ? 'none' : 'flex';
toggleBtn.textContent = isVisible ? 'Setup' : 'Close';
if (!isVisible) {
if (gisContainer) {
gisContainer.innerHTML = '';
gisContainer.style.display = 'none';
}
if (setupGisBtn) {
setupGisBtn.style.display = '';
}
}
});
setupGisBtn.addEventListener('click', () => {
const clientId = clientIdInput.value;
if (!clientId) {
alert('Please enter an OAuth Client ID first.');
return;
}
renderGoogleSignInButton(toolId, clientId, authProfileName);
});
return item;
}

View File

@@ -0,0 +1,173 @@
// 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 { renderToolInterface } from "./toolDisplay.js";
let toolDetailsAbortController = null;
/**
* Fetches a toolset from the /api/toolset endpoint and initiates creating the tool list.
* @param {!HTMLElement} secondNavContent The HTML element where the tool list will be rendered.
* @param {!HTMLElement} toolDisplayArea The HTML element where the details of a selected tool will be displayed.
* @param {string} toolsetName The name of the toolset to load (empty string loads all tools).
* @returns {!Promise<void>} A promise that resolves when the tools are loaded and rendered, or rejects on error.
*/
export async function loadTools(secondNavContent, toolDisplayArea, toolsetName) {
secondNavContent.innerHTML = '<p>Fetching tools...</p>';
try {
const response = await fetch(`/api/toolset/${toolsetName}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const apiResponse = await response.json();
renderToolList(apiResponse, secondNavContent, toolDisplayArea);
} catch (error) {
console.error('Failed to load tools:', error);
secondNavContent.innerHTML = `<p class="error">Failed to load tools: <pre><code>${error}</code></pre></p>`;
}
}
/**
* Renders the list of tools as buttons within the provided HTML element.
* @param {?{tools: ?Object<string,*>} } apiResponse The API response object containing the tools.
* @param {!HTMLElement} secondNavContent The HTML element to render the tool list into.
* @param {!HTMLElement} toolDisplayArea The HTML element for displaying tool details (passed to event handlers).
*/
function renderToolList(apiResponse, secondNavContent, toolDisplayArea) {
secondNavContent.innerHTML = '';
if (!apiResponse || typeof apiResponse.tools !== 'object' || apiResponse.tools === null) {
console.error('Error: Expected an object with a "tools" property, but received:', apiResponse);
secondNavContent.textContent = 'Error: Invalid response format from toolset API.';
return;
}
const toolsObject = apiResponse.tools;
const toolNames = Object.keys(toolsObject);
if (toolNames.length === 0) {
secondNavContent.textContent = 'No tools found.';
return;
}
const ul = document.createElement('ul');
toolNames.forEach(toolName => {
const li = document.createElement('li');
const button = document.createElement('button');
button.textContent = toolName;
button.dataset.toolname = toolName;
button.classList.add('tool-button');
button.addEventListener('click', (event) => handleToolClick(event, secondNavContent, toolDisplayArea));
li.appendChild(button);
ul.appendChild(li);
});
secondNavContent.appendChild(ul);
}
/**
* Handles the click event on a tool button.
* @param {!Event} event The click event object.
* @param {!HTMLElement} secondNavContent The parent element containing the tool buttons.
* @param {!HTMLElement} toolDisplayArea The HTML element where tool details will be shown.
*/
function handleToolClick(event, secondNavContent, toolDisplayArea) {
const toolName = event.target.dataset.toolname;
if (toolName) {
const currentActive = secondNavContent.querySelector('.tool-button.active');
if (currentActive) {
currentActive.classList.remove('active');
}
event.target.classList.add('active');
fetchToolDetails(toolName, toolDisplayArea);
}
}
/**
* Fetches details for a specific tool /api/tool endpoint.
* It aborts any previous in-flight request for tool details to stop race condition.
* @param {string} toolName The name of the tool to fetch details for.
* @param {!HTMLElement} toolDisplayArea The HTML element to display the tool interface in.
* @returns {!Promise<void>} A promise that resolves when the tool details are fetched and rendered, or rejects on error.
*/
async function fetchToolDetails(toolName, toolDisplayArea) {
if (toolDetailsAbortController) {
toolDetailsAbortController.abort();
console.debug("Aborted previous tool fetch.");
}
toolDetailsAbortController = new AbortController();
const signal = toolDetailsAbortController.signal;
toolDisplayArea.innerHTML = '<p>Loading tool details...</p>';
try {
const response = await fetch(`/api/tool/${encodeURIComponent(toolName)}`, { signal });
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const apiResponse = await response.json();
if (!apiResponse.tools || !apiResponse.tools[toolName]) {
throw new Error(`Tool "${toolName}" data not found in API response.`);
}
const toolObject = apiResponse.tools[toolName];
console.debug("Received tool object: ", toolObject)
const toolInterfaceData = {
id: toolName,
name: toolName,
description: toolObject.description || "No description provided.",
parameters: (toolObject.parameters || []).map(param => {
let inputType = 'text';
const apiType = param.type ? param.type.toLowerCase() : 'string';
let valueType = 'string';
let label = param.description || param.name;
if (apiType === 'integer' || apiType === 'float') {
inputType = 'number';
valueType = 'number';
} else if (apiType === 'boolean') {
inputType = 'checkbox';
valueType = 'boolean';
} else if (apiType === 'array') {
inputType = 'textarea';
const itemType = param.items && param.items.type ? param.items.type.toLowerCase() : 'string';
valueType = `array<${itemType}>`;
label += ' (Array)';
}
return {
name: param.name,
type: inputType,
valueType: valueType,
label: label,
authServices: param.authSources,
required: param.required || false,
// defaultValue: param.default, can't do this yet bc tool manifest doesn't have default
};
})
};
console.debug("Transformed toolInterfaceData:", toolInterfaceData);
renderToolInterface(toolInterfaceData, toolDisplayArea);
} catch (error) {
if (error.name === 'AbortError') {
console.debug("Previous fetch was aborted, expected behavior.");
} else {
console.error(`Failed to load details for tool "${toolName}":`, error);
toolDisplayArea.innerHTML = `<p class="error">Failed to load details for ${toolName}. ${error.message}</p>`;
}
}
}

View File

@@ -0,0 +1,40 @@
// 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.
/**
* Renders the main content area into the HTML.
* @param {string} containerId The ID of the DOM element to inject the content into.
* @param {string} idString The id of the item inside the main content area.
*/
function renderMainContent(containerId, idString) {
const mainContentContainer = document.getElementById(containerId);
if (!mainContentContainer) {
console.error(`Content container with ID "${containerId}" not found.`);
return;
}
const idAttribute = idString ? `id="${idString}"` : '';
const contentHTML = `
<div class="main-content-area">
<div class="top-bar">
</div>
<main class="content" ${idAttribute}">
<h1>Welcome to MCP Toolbox UI</h1>
<p>This is the main content area. Click a tab on the left to navigate.</p>
</main>
</div>
`;
mainContentContainer.innerHTML = contentHTML;
}

View File

@@ -0,0 +1,53 @@
// 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.
/**
* Renders the navigation bar HTML content into the specified container element.
* @param {string} containerId The ID of the DOM element to inject the navbar into.
* @param {string | null} activePath The active tab from the navbar.
*/
function renderNavbar(containerId, activePath) {
const navbarContainer = document.getElementById(containerId);
if (!navbarContainer) {
console.error(`Navbar container with ID "${containerId}" not found.`);
return;
}
const navbarHTML = `
<nav class="left-nav">
<div class="nav-logo">
<img src="/ui/assets/mcptoolboxlogo.png" alt="App Logo">
</div>
<ul>
<li><a href="/ui/sources">Sources</a></li>
<li><a href="/ui/authservices">Auth Services</a></li>
<li><a href="/ui/tools">Tools</a></li>
<li><a href="/ui/toolsets">Toolsets</a></li>
</ul>
</nav>
`;
navbarContainer.innerHTML = navbarHTML;
if (activePath) {
const navLinks = navbarContainer.querySelectorAll('.left-nav ul li a');
navLinks.forEach(link => {
const linkPath = new URL(link.href).pathname;
if (linkPath === activePath) {
link.classList.add('active');
} else {
link.classList.remove('active');
}
});
}
}

View File

@@ -0,0 +1,162 @@
// 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 { isParamIncluded } from "./toolDisplay.js";
/**
* Runs a specific tool using the /api/tools/toolName/invoke endpoint
* @param {string} toolId The unique identifier for the tool.
* @param {!HTMLFormElement} form The form element containing parameter inputs.
* @param {!HTMLTextAreaElement} responseArea The textarea to display results or errors.
* @param {!Array<!Object>} parameters An array of parameter definition objects
* @param {!HTMLInputElement} prettifyCheckbox The checkbox to control JSON formatting.
* @param {function(?Object): void} updateLastResults Callback to store the last results.
*/
export async function handleRunTool(toolId, form, responseArea, parameters, prettifyCheckbox, updateLastResults, headers) {
const formData = new FormData(form);
const typedParams = {};
responseArea.value = 'Running tool...';
updateLastResults(null);
for (const param of parameters) {
const NAME = param.name;
const VALUE_TYPE = param.valueType;
const RAW_VALUE = formData.get(NAME);
const INCLUDE_CHECKED = isParamIncluded(toolId, NAME)
try {
if (!INCLUDE_CHECKED) {
console.debug(`Param ${NAME} was intentionally skipped.`)
// if param was purposely unchecked, don't include it in body
continue;
}
if (VALUE_TYPE === 'boolean') {
typedParams[NAME] = RAW_VALUE !== null;
console.debug(`Parameter ${NAME} (boolean) set to: ${typedParams[NAME]}`);
continue;
}
// process remaining types
if (VALUE_TYPE && VALUE_TYPE.startsWith('array<')) {
typedParams[NAME] = parseArrayParameter(RAW_VALUE, VALUE_TYPE, NAME);
} else {
switch (VALUE_TYPE) {
case 'number':
if (RAW_VALUE === "") {
console.debug(`Param ${NAME} was empty, setting to empty string.`)
typedParams[NAME] = "";
} else {
const num = Number(RAW_VALUE);
if (isNaN(num)) {
throw new Error(`Invalid number input for ${NAME}: ${RAW_VALUE}`);
}
typedParams[NAME] = num;
}
break;
case 'string':
default:
typedParams[NAME] = RAW_VALUE;
break;
}
}
} catch (error) {
console.error('Error processing parameter:', NAME, error);
responseArea.value = `Error for ${NAME}: ${error.message}`;
return;
}
}
console.debug('Running tool:', toolId, 'with typed params:', typedParams);
try {
const response = await fetch(`/api/tool/${toolId}/invoke`, {
method: 'POST',
headers: headers,
body: JSON.stringify(typedParams)
});
if (!response.ok) {
const errorBody = await response.text();
throw new Error(`HTTP error ${response.status}: ${errorBody}`);
}
const results = await response.json();
updateLastResults(results);
displayResults(results, responseArea, prettifyCheckbox.checked);
} catch (error) {
console.error('Error running tool:', error);
responseArea.value = `Error: ${error.message}`;
updateLastResults(null);
}
}
/**
* Parses and validates a single array parameter from a raw string value.
* @param {string} rawValue The raw string value from FormData.
* @param {string} valueType The full array type string (e.g., "array<number>").
* @param {string} paramName The name of the parameter for error messaging.
* @return {!Array<*>} The parsed array.
* @throws {Error} If parsing or type validation fails.
*/
function parseArrayParameter(rawValue, valueType, paramName) {
const ELEMENT_TYPE = valueType.substring(6, valueType.length - 1);
let parsedArray;
try {
parsedArray = JSON.parse(rawValue);
} catch (e) {
throw new Error(`Invalid JSON format for ${paramName}. Expected an array. ${e.message}`);
}
if (!Array.isArray(parsedArray)) {
throw new Error(`Input for ${paramName} must be a JSON array (e.g., ["a", "b"]).`);
}
return parsedArray.map((item, index) => {
switch (ELEMENT_TYPE) {
case 'number':
const NUM = Number(item);
if (isNaN(NUM)) {
throw new Error(`Invalid number "${item}" found in array for ${paramName} at index ${index}.`);
}
return NUM;
case 'boolean':
return item === true || String(item).toLowerCase() === 'true';
case 'string':
default:
return item;
}
});
}
/**
* Displays the results from the tool run in the response area.
*/
export function displayResults(results, responseArea, prettify) {
if (results === null || results === undefined) {
return;
}
try {
const resultJson = JSON.parse(results.result);
if (prettify) {
responseArea.value = JSON.stringify(resultJson, null, 2);
} else {
responseArea.value = JSON.stringify(resultJson);
}
} catch (error) {
console.error("Error parsing or stringifying results:", error);
if (typeof results.result === 'string') {
responseArea.value = results.result;
} else {
responseArea.value = "Error displaying results. Invalid format.";
}
}
}

View File

@@ -0,0 +1,541 @@
// 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 { handleRunTool, displayResults } from './runTool.js';
import { createGoogleAuthMethodItem } from './auth.js'
/**
* Helper function to create form inputs for parameters.
*/
function createParamInput(param, toolId) {
const paramItem = document.createElement('div');
paramItem.className = 'param-item';
const label = document.createElement('label');
const INPUT_ID = `param-${toolId}-${param.name}`;
const NAME_TEXT = document.createTextNode(param.name);
label.setAttribute('for', INPUT_ID);
label.appendChild(NAME_TEXT);
const IS_AUTH_PARAM = param.authServices && param.authServices.length > 0;
let additionalLabelText = '';
if (IS_AUTH_PARAM) {
additionalLabelText += ' (auth)';
}
if (!param.required) {
additionalLabelText += ' (optional)';
}
if (additionalLabelText) {
const additionalSpan = document.createElement('span');
additionalSpan.textContent = additionalLabelText;
additionalSpan.classList.add('param-label-extras');
label.appendChild(additionalSpan);
}
paramItem.appendChild(label);
const inputCheckboxWrapper = document.createElement('div');
const inputContainer = document.createElement('div');
inputCheckboxWrapper.className = 'input-checkbox-wrapper';
inputContainer.className = 'param-input-element-container';
// Build parameter's value input box.
const PLACEHOLDER_LABEL = param.label;
let inputElement;
let boolValueLabel = null;
if (param.type === 'textarea') {
inputElement = document.createElement('textarea');
inputElement.rows = 3;
inputContainer.appendChild(inputElement);
} else if(param.type === 'checkbox') {
inputElement = document.createElement('input');
inputElement.type = 'checkbox';
inputElement.title = PLACEHOLDER_LABEL;
inputElement.checked = false;
// handle true/false label for boolean params
boolValueLabel = document.createElement('span');
boolValueLabel.className = 'checkbox-bool-label';
boolValueLabel.textContent = inputElement.checked ? ' true' : ' false';
inputContainer.appendChild(inputElement);
inputContainer.appendChild(boolValueLabel);
inputElement.addEventListener('change', () => {
boolValueLabel.textContent = inputElement.checked ? ' true' : ' false';
});
} else {
inputElement = document.createElement('input');
inputElement.type = param.type;
inputContainer.appendChild(inputElement);
}
inputElement.id = INPUT_ID;
inputElement.name = param.name;
inputElement.classList.add('param-input-element');
if (IS_AUTH_PARAM) {
inputElement.disabled = true;
inputElement.classList.add('auth-param-input');
if (param.type !== 'checkbox') {
inputElement.placeholder = param.authServices;
}
} else if (param.type !== 'checkbox') {
inputElement.placeholder = PLACEHOLDER_LABEL ? PLACEHOLDER_LABEL.trim() : '';
}
inputCheckboxWrapper.appendChild(inputContainer);
// create the "Include Param" checkbox
const INCLUDE_CHECKBOX_ID = `include-${INPUT_ID}`;
const includeContainer = document.createElement('div');
const includeCheckbox = document.createElement('input');
includeContainer.className = 'include-param-container';
includeCheckbox.type = 'checkbox';
includeCheckbox.id = INCLUDE_CHECKBOX_ID;
includeCheckbox.name = `include-${param.name}`;
includeCheckbox.title = 'Include this parameter'; // Add a tooltip
// default to checked, unless it's an optional parameter
includeCheckbox.checked = param.required;
includeContainer.appendChild(includeCheckbox);
inputCheckboxWrapper.appendChild(includeContainer);
paramItem.appendChild(inputCheckboxWrapper);
// function to update UI based on checkbox state
const updateParamIncludedState = () => {
const isIncluded = includeCheckbox.checked;
if (isIncluded) {
paramItem.classList.remove('disabled-param');
if (!IS_AUTH_PARAM) {
inputElement.disabled = false;
}
if (boolValueLabel) {
boolValueLabel.classList.remove('disabled');
}
} else {
paramItem.classList.add('disabled-param');
inputElement.disabled = true;
if (boolValueLabel) {
boolValueLabel.classList.add('disabled');
}
}
};
// add event listener to the include checkbox
includeCheckbox.addEventListener('change', updateParamIncludedState);
updateParamIncludedState();
return paramItem;
}
/**
* Function to create the header editor popup modal.
* @param {string} toolId The unique identifier for the tool.
* @param {!Object<string, string>} currentHeaders The current headers.
* @param {function(!Object<string, string>): void} saveCallback A function to be
* called when the "Save" button is clicked and the headers are successfully
* parsed. The function receives the updated headers object as its argument.
* @return {!HTMLDivElement} The outermost div element of the created modal.
*/
function createHeaderEditorModal(toolId, currentHeaders, toolParameters, saveCallback) {
const MODAL_ID = `header-modal-${toolId}`;
let modal = document.getElementById(MODAL_ID);
if (modal) {
modal.remove();
}
modal = document.createElement('div');
modal.id = MODAL_ID;
modal.className = 'header-modal';
const modalContent = document.createElement('div');
const modalHeader = document.createElement('h5');
const headersTextarea = document.createElement('textarea');
modalContent.className = 'header-modal-content';
modalHeader.textContent = 'Edit Request Headers';
headersTextarea.id = `headers-textarea-${toolId}`;
headersTextarea.className = 'headers-textarea';
headersTextarea.rows = 10;
headersTextarea.value = JSON.stringify(currentHeaders, null, 2);
const authProfileNames = new Set();
toolParameters.forEach(param => {
const isAuthParam = param.authServices && param.authServices.length > 0;
if (isAuthParam && param.authServices) {
param.authServices.forEach(name => authProfileNames.add(name));
}
});
modalContent.appendChild(modalHeader);
modalContent.appendChild(headersTextarea);
if (authProfileNames.size > 0) {
const authHelperSection = document.createElement('div');
authHelperSection.className = 'auth-helper-section';
const title = document.createElement('h6');
title.className = 'auth-helper-title';
title.textContent = 'Authentication Helpers';
authHelperSection.appendChild(title);
const authList = document.createElement('div');
authList.className = 'auth-method-list';
authProfileNames.forEach(profileName => {
if (profileName.toLowerCase().includes('google')) {
const authItem = createGoogleAuthMethodItem(toolId, profileName);
authList.appendChild(authItem);
} else {
console.warn(`Unsupported auth service type for helper UI: ${profileName}`);
}
});
authHelperSection.appendChild(authList);
modalContent.appendChild(authHelperSection);
}
const modalActions = document.createElement('div');
const closeButton = document.createElement('button');
const saveButton = document.createElement('button');
const authTokenDropdown = createAuthTokenInfoDropdown();
modalActions.className = 'header-modal-actions';
closeButton.textContent = 'Close';
closeButton.className = 'btn btn--closeHeaders';
closeButton.addEventListener('click', () => closeHeaderEditor(toolId));
saveButton.textContent = 'Save';
saveButton.className = 'btn btn--saveHeaders';
saveButton.addEventListener('click', () => {
try {
const updatedHeaders = JSON.parse(headersTextarea.value);
saveCallback(updatedHeaders);
closeHeaderEditor(toolId);
} catch (e) {
alert('Invalid JSON format for headers.');
console.error("Header JSON parse error:", e);
}
});
modalActions.appendChild(closeButton);
modalActions.appendChild(saveButton);
modalContent.appendChild(modalActions);
modalContent.appendChild(authTokenDropdown);
modal.appendChild(modalContent);
// Close modal if clicked outside
window.addEventListener('click', (event) => {
if (event.target === modal) {
closeHeaderEditor(toolId);
}
});
return modal;
}
/**
* Function to open the header popup.
*/
function openHeaderEditor(toolId) {
const modal = document.getElementById(`header-modal-${toolId}`);
if (modal) {
modal.style.display = 'block';
}
}
/**
* Function to close the header popup.
*/
function closeHeaderEditor(toolId) {
const modal = document.getElementById(`header-modal-${toolId}`);
if (modal) {
modal.style.display = 'none';
}
}
/**
* Creates a dropdown element showing information on how to extract Google auth tokens.
* @return {HTMLDetailsElement} The details element representing the dropdown.
*/
function createAuthTokenInfoDropdown() {
const details = document.createElement('details');
const summary = document.createElement('summary');
const content = document.createElement('div');
details.className = 'auth-token-details';
details.appendChild(summary);
summary.textContent = 'How to extract Google OAuth ID Token manually';
content.className = 'auth-token-content';
// auth instruction dropdown
const tabButtons = document.createElement('div');
const leftTab = document.createElement('button');
const rightTab = document.createElement('button');
tabButtons.className = 'auth-tab-group';
leftTab.className = 'auth-tab-picker active';
leftTab.textContent = 'With Standard Account';
leftTab.setAttribute('data-tab', 'standard');
rightTab.className = 'auth-tab-picker';
rightTab.textContent = 'With Service Account';
rightTab.setAttribute('data-tab', 'service');
tabButtons.appendChild(leftTab);
tabButtons.appendChild(rightTab);
content.appendChild(tabButtons);
const tabContentContainer = document.createElement('div');
const standardAccInstructions = document.createElement('div');
const serviceAccInstructions = document.createElement('div');
standardAccInstructions.id = 'auth-tab-standard';
standardAccInstructions.className = 'auth-tab-content active';
standardAccInstructions.innerHTML = AUTH_TOKEN_INSTRUCTIONS_STANDARD;
serviceAccInstructions.id = 'auth-tab-service';
serviceAccInstructions.className = 'auth-tab-content';
serviceAccInstructions.innerHTML = AUTH_TOKEN_INSTRUCTIONS_SERVICE_ACCOUNT;
tabContentContainer.appendChild(standardAccInstructions);
tabContentContainer.appendChild(serviceAccInstructions);
content.appendChild(tabContentContainer);
// switching tabs logic
const tabBtns = [leftTab, rightTab];
const tabContents = [standardAccInstructions, serviceAccInstructions];
tabBtns.forEach(btn => {
btn.addEventListener('click', () => {
// deactivate all buttons and contents
tabBtns.forEach(b => b.classList.remove('active'));
tabContents.forEach(c => c.classList.remove('active'));
btn.classList.add('active');
const tabId = btn.getAttribute('data-tab');
const activeContent = content.querySelector(`#auth-tab-${tabId}`);
if (activeContent) {
activeContent.classList.add('active');
}
});
});
details.appendChild(content);
return details;
}
/**
* Renders the tool display area.
*/
export function renderToolInterface(tool, containerElement) {
const TOOL_ID = tool.id;
containerElement.innerHTML = '';
let lastResults = null;
let currentHeaders = {
"Content-Type": "application/json"
};
// function to update lastResults so we can toggle json
const updateLastResults = (newResults) => {
lastResults = newResults;
};
const updateCurrentHeaders = (newHeaders) => {
currentHeaders = newHeaders;
const newModal = createHeaderEditorModal(TOOL_ID, currentHeaders, tool.parameters, updateCurrentHeaders);
containerElement.appendChild(newModal);
};
const gridContainer = document.createElement('div');
gridContainer.className = 'tool-details-grid';
const toolInfoContainer = document.createElement('div');
const nameBox = document.createElement('div');
const descBox = document.createElement('div');
nameBox.className = 'tool-box tool-name';
nameBox.innerHTML = `<h5>Name:</h5><p>${tool.name}</p>`;
descBox.className = 'tool-box tool-description';
descBox.innerHTML = `<h5>Description:</h5><p>${tool.description}</p>`;
toolInfoContainer.className = 'tool-info';
toolInfoContainer.appendChild(nameBox);
toolInfoContainer.appendChild(descBox);
gridContainer.appendChild(toolInfoContainer);
const DISLCAIMER_INFO = "*Checked parameters are sent with the value from their text field. Empty fields will be sent as an empty string. To exclude a parameter, uncheck it."
const paramsContainer = document.createElement('div');
const form = document.createElement('form');
const paramsHeader = document.createElement('div');
const disclaimerText = document.createElement('div');
paramsContainer.className = 'tool-params tool-box';
paramsContainer.innerHTML = '<h5>Parameters:</h5>';
paramsHeader.className = 'params-header';
paramsContainer.appendChild(paramsHeader);
disclaimerText.textContent = DISLCAIMER_INFO;
disclaimerText.className = 'params-disclaimer';
paramsContainer.appendChild(disclaimerText);
form.id = `tool-params-form-${TOOL_ID}`;
tool.parameters.forEach(param => {
form.appendChild(createParamInput(param, TOOL_ID));
});
paramsContainer.appendChild(form);
gridContainer.appendChild(paramsContainer);
containerElement.appendChild(gridContainer);
const RESPONSE_AREA_ID = `tool-response-area-${TOOL_ID}`;
const runButtonContainer = document.createElement('div');
const editHeadersButton = document.createElement('button');
const runButton = document.createElement('button');
editHeadersButton.className = 'btn btn--editHeaders';
editHeadersButton.textContent = 'Edit Headers';
editHeadersButton.addEventListener('click', () => openHeaderEditor(TOOL_ID));
runButtonContainer.className = 'run-button-container';
runButtonContainer.appendChild(editHeadersButton);
runButton.className = 'btn btn--run';
runButton.textContent = 'Run Tool';
runButtonContainer.appendChild(runButton);
containerElement.appendChild(runButtonContainer);
// response Area (bottom)
const responseContainer = document.createElement('div');
const responseHeaderControls = document.createElement('div');
const responseHeader = document.createElement('h5');
const responseArea = document.createElement('textarea');
responseContainer.className = 'tool-response tool-box';
responseHeaderControls.className = 'response-header-controls';
responseHeader.textContent = 'Response:';
responseHeaderControls.appendChild(responseHeader);
// prettify box
const PRETTIFY_ID = `prettify-${TOOL_ID}`;
const prettifyDiv = document.createElement('div');
const prettifyLabel = document.createElement('label');
const prettifyCheckbox = document.createElement('input');
prettifyDiv.className = 'prettify-container';
prettifyLabel.setAttribute('for', PRETTIFY_ID);
prettifyLabel.textContent = 'Prettify JSON';
prettifyLabel.className = 'prettify-label';
prettifyCheckbox.type = 'checkbox';
prettifyCheckbox.id = PRETTIFY_ID;
prettifyCheckbox.checked = true;
prettifyCheckbox.className = 'prettify-checkbox';
prettifyDiv.appendChild(prettifyLabel);
prettifyDiv.appendChild(prettifyCheckbox);
responseHeaderControls.appendChild(prettifyDiv);
responseContainer.appendChild(responseHeaderControls);
responseArea.id = RESPONSE_AREA_ID;
responseArea.readOnly = true;
responseArea.placeholder = 'Results will appear here...';
responseArea.className = 'tool-response-area';
responseArea.rows = 10;
responseContainer.appendChild(responseArea);
containerElement.appendChild(responseContainer);
// create and append the header editor modal
const headerModal = createHeaderEditorModal(TOOL_ID, currentHeaders, tool.parameters, updateCurrentHeaders);
containerElement.appendChild(headerModal);
prettifyCheckbox.addEventListener('change', () => {
if (lastResults) {
displayResults(lastResults, responseArea, prettifyCheckbox.checked);
}
});
runButton.addEventListener('click', (event) => {
event.preventDefault();
handleRunTool(TOOL_ID, form, responseArea, tool.parameters, prettifyCheckbox, updateLastResults, currentHeaders);
});
}
/**
* Checks if a specific parameter is marked as included for a given tool.
* @param {string} toolId The ID of the tool.
* @param {string} paramName The name of the parameter.
* @return {boolean|null} True if the parameter's include checkbox is checked,
* False if unchecked, Null if the checkbox element is not found.
*/
export function isParamIncluded(toolId, paramName) {
const inputId = `param-${toolId}-${paramName}`;
const includeCheckboxId = `include-${inputId}`;
const includeCheckbox = document.getElementById(includeCheckboxId);
if (includeCheckbox && includeCheckbox.type === 'checkbox') {
return includeCheckbox.checked;
}
console.warn(`Include checkbox not found for ID: ${includeCheckboxId}`);
return null;
}
// Templates for inserting token retrieval instructions into edit header modal
const AUTH_TOKEN_INSTRUCTIONS_SERVICE_ACCOUNT = `
<p>To obtain a Google OAuth ID token using a service account:</p>
<ol>
<li>Make sure you are on the intended SERVICE account (typically contain iam.gserviceaccount.com). Verify by running the command below.
<pre><code>gcloud auth list</code></pre>
</li>
<li>Print an id token with the audience set to your clientID defined in tools file:
<pre><code>gcloud auth print-identity-token --audiences=YOUR_CLIENT_ID_HERE</code></pre>
</li>
<li>Copy the output token.</li>
<li>Paste this token into the header in JSON editor. The key should be the name of your auth service followed by <code>_token</code>
<pre><code>{
"Content-Type": "application/json",
"my-google-auth_token": "YOUR_ID_TOKEN_HERE"
} </code></pre>
</li>
</ol>
<p>This token is typically short-lived.</p>`;
const AUTH_TOKEN_INSTRUCTIONS_STANDARD = `
<p>To obtain a Google OAuth ID token using a standard account:</p>
<ol>
<li>Make sure you are on your intended standard account. Verify by running the command below.
<pre><code>gcloud auth list</code></pre>
</li>
<li>Within your Cloud Console, add the following link to the "Authorized Redirect URIs".</li>
<pre><code>https://developers.google.com/oauthplayground</code></pre>
<li>Go to the Google OAuth Playground site: <a href="https://developers.google.com/oauthplayground/" target="_blank">https://developers.google.com/oauthplayground/</a></li>
<li>In the top right settings menu, select "Use your own OAuth Credentials".</li>
<li>Input your clientID (from tools file), along with the client secret from Cloud Console.</li>
<li>Inside the Google OAuth Playground, select "Google OAuth2 API v2.</li>
<ul>
<li>Select "Authorize APIs".</li>
<li>Select "Exchange Authorization codes for tokens"</li>
<li>Copy the id_token field provided in the response.</li>
</ul>
<li>Paste this token into the header in JSON editor. The key should be the name of your auth service followed by <code>_token</code>
<pre><code>{
"Content-Type": "application/json",
"my-google-auth_token": "YOUR_ID_TOKEN_HERE"
} </code></pre>
</li>
</ol>
<p>This token is typically short-lived.</p>`;

View File

@@ -0,0 +1,32 @@
// 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 { loadTools } from "./loadTools.js";
/**
* These functions runs after the browser finishes loading and parsing HTML structure.
* This ensures that elements can be safely accessed.
*/
document.addEventListener('DOMContentLoaded', () => {
const toolDisplayArea = document.getElementById('tool-display-area');
const secondaryPanelContent = document.getElementById('secondary-panel-content');
const DEFAULT_TOOLSET = ""; // will return all toolsets
if (!secondaryPanelContent || !toolDisplayArea) {
console.error('Required DOM elements not found.');
return;
}
loadTools(secondaryPanelContent, toolDisplayArea, DEFAULT_TOOLSET);
});

View File

@@ -0,0 +1,49 @@
// 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 { loadTools } from "./loadTools.js";
document.addEventListener('DOMContentLoaded', () => {
const searchInput = document.getElementById('toolset-search-input');
const searchButton = document.getElementById('toolset-search-button');
const secondNavContent = document.getElementById('secondary-panel-content');
const toolDisplayArea = document.getElementById('tool-display-area');
if (!searchInput || !searchButton || !secondNavContent || !toolDisplayArea) {
console.error('Required DOM elements not found.');
return;
}
// Event listener for search button click
searchButton.addEventListener('click', () => {
const toolsetName = searchInput.value.trim();
if (toolsetName) {
loadTools(secondNavContent, toolDisplayArea, toolsetName)
} else {
secondNavContent.innerHTML = '<p>Please enter a toolset name to search.</p>';
}
});
// Event listener for Enter key in search input
searchInput.addEventListener('keypress', (event) => {
if (event.key === 'Enter') {
const toolsetName = searchInput.value.trim();
if (toolsetName) {
loadTools(secondNavContent, toolDisplayArea, toolsetName);
} else {
secondNavContent.innerHTML = '<p>Please enter a toolset name to search.</p>';
}
}
});
})

View File

@@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tools View</title>
<link rel="stylesheet" href="/ui/css/style.css">
<script src="https://accounts.google.com/gsi/client" async defer></script>
</head>
<body>
<div id="navbar-container" data-active-nav="/ui/tools"></div>
<aside class="second-nav">
<h4>My Tools</h4>
<div id="secondary-panel-content">
<p>Fetching tools...</p>
</div>
</aside>
<div id="main-content-container"></div>
<script type="module" src="/ui/js/tools.js"></script>
<script src="/ui/js/navbar.js"></script>
<script src="/ui/js/mainContent.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const navbarContainer = document.getElementById('navbar-container');
const activeNav = navbarContainer.getAttribute('data-active-nav');
renderNavbar('navbar-container', activeNav);
renderMainContent('main-content-container', 'tool-display-area')
});
</script>
</body>
</html>

View File

@@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Toolsets View</title>
<link rel="stylesheet" href="/ui/css/style.css">
</head>
<body>
<div id="navbar-container" data-active-nav="/ui/toolsets"></div>
<aside class="second-nav">
<h4>Search Toolsets</h4>
<div class="search-container">
<input type="text" id="toolset-search-input" placeholder="Enter toolset name...">
<button id="toolset-search-button">Search</button>
</div>
<div id="secondary-panel-content">
<p>Search for a toolset to see available tools.</p>
</div>
</aside>
<div id="main-content-container"></div>
<script type="module" src="/ui/js/toolsets.js"></script>
<script src="/ui/js/navbar.js"></script>
<script src="/ui/js/mainContent.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const navbarContainer = document.getElementById('navbar-container');
const activeNav = navbarContainer.getAttribute('data-active-nav');
renderNavbar('navbar-container', activeNav);
renderMainContent('main-content-container', 'tool-display-area');
});
</script>
</body>
</html>

164
internal/server/web.go Normal file
View File

@@ -0,0 +1,164 @@
package server
import (
"bytes"
"context"
"embed"
"encoding/json"
"fmt"
"io"
"io/fs"
"net/http"
"sync"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/google/uuid"
"github.com/googleapis/genai-toolbox/internal/server/agent"
)
//go:embed all:static
var staticContent embed.FS
type session struct {
events chan agent.ChatEvent
}
var (
sessions = struct {
sync.RWMutex
m map[string]*session
}{m: make(map[string]*session)}
)
func webRouter() (chi.Router, error) {
r := chi.NewRouter()
r.Use(middleware.StripSlashes)
// HTML entry points
r.Get("/", func(w http.ResponseWriter, r *http.Request) { serveHTML(w, r, "static/index.html") })
r.Get("/tools", func(w http.ResponseWriter, r *http.Request) { serveHTML(w, r, "static/tools.html") })
r.Get("/toolsets", func(w http.ResponseWriter, r *http.Request) { serveHTML(w, r, "static/toolsets.html") })
r.Get("/agent", func(w http.ResponseWriter, r *http.Request) { serveHTML(w, r, "static/agent.html") })
// Chat endpoints -------------------------------------------------
r.Post("/chat", startChatHandler) // POST /ui/chat
r.Get("/chat/{id}/events", streamChatHandler) // GET /ui/chat/{id}/events
// static assets
staticFS, _ := fs.Sub(staticContent, "static")
r.Handle("/*", http.StripPrefix("/ui", http.FileServer(http.FS(staticFS))))
return r, nil
}
type startReq struct {
Message string `json:"message"`
}
type startResp struct {
ID string `json:"id"`
}
func startChatHandler(w http.ResponseWriter, r *http.Request) {
var req startReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Message == "" {
http.Error(w, "invalid body: need {\"message\":\"...\"}", http.StatusBadRequest)
return
}
eng, err := getEngine(r.Context())
if err != nil {
http.Error(w, "engine init: "+err.Error(), http.StatusInternalServerError)
return
}
// create session
id := uuid.NewString()
s := &session{events: make(chan agent.ChatEvent, 32)}
sessions.Lock()
sessions.m[id] = s
sessions.Unlock()
// go eng.Run(r.Context(), req.Message, s.events)
go eng.Run(context.Background(), req.Message, s.events)
_ = json.NewEncoder(w).Encode(startResp{ID: id})
}
func streamChatHandler(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
sessions.RLock()
s, ok := sessions.m[id]
sessions.RUnlock()
if !ok {
http.NotFound(w, r)
return
}
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(w, "stream unsupported", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
ctx := r.Context()
for {
select {
case <-ctx.Done():
return
case ev, open := <-s.events:
if !open {
return // chat finished
}
b, _ := json.Marshal(ev)
fmt.Fprintf(w, "event: %s\ndata: %s\n\n", ev.Type, b)
flusher.Flush()
}
}
}
var (
engineOnce sync.Once
globalEng *agent.Engine
engineErr error
)
func getEngine(ctx context.Context) (*agent.Engine, error) {
engineOnce.Do(func() {
genaiKey := os.Getenv("GOOGLE_API_KEY")
toolboxURL := "http://localhost:5000"
toolsetID := "my-toolset-5"
globalEng, engineErr = agent.New(ctx, genaiKey, toolboxURL, toolsetID)
})
return globalEng, engineErr
}
func serveHTML(w http.ResponseWriter, r *http.Request, filepath string) {
file, err := staticContent.Open(filepath)
if err != nil {
http.Error(w, "File not found", http.StatusNotFound)
return
}
defer file.Close()
fileBytes, err := io.ReadAll(file)
if err != nil {
http.Error(w, fmt.Sprintf("Error reading file: %v", err), http.StatusInternalServerError)
return
}
fileInfo, err := file.Stat()
if err != nil {
return
}
http.ServeContent(w, r, fileInfo.Name(), fileInfo.ModTime(), bytes.NewReader(fileBytes))
}

179
internal/server/web_test.go Normal file
View File

@@ -0,0 +1,179 @@
package server
import (
"io"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"github.com/go-chi/chi/v5"
"github.com/go-goquery/goquery"
)
// TestWebEndpoint tests the routes defined in webRouter mounted under /ui.
func TestWebEndpoint(t *testing.T) {
mainRouter := chi.NewRouter()
webR, err := webRouter()
if err != nil {
t.Fatalf("Failed to create webRouter: %v", err)
}
mainRouter.Mount("/ui", webR)
ts := httptest.NewServer(mainRouter)
defer ts.Close()
testCases := []struct {
name string
path string
wantStatus int
wantContentType string
wantPageTitle string
}{
{
name: "web index page",
path: "/ui",
wantStatus: http.StatusOK,
wantContentType: "text/html",
wantPageTitle: "Toolbox UI",
},
{
name: "web index page with trailing slash",
path: "/ui/",
wantStatus: http.StatusOK,
wantContentType: "text/html",
wantPageTitle: "Toolbox UI",
},
{
name: "web tools page",
path: "/ui/tools",
wantStatus: http.StatusOK,
wantContentType: "text/html",
wantPageTitle: "Tools View",
},
{
name: "web tools page with trailing slash",
path: "/ui/tools/",
wantStatus: http.StatusOK,
wantContentType: "text/html",
wantPageTitle: "Tools View",
},
{
name: "web toolsets page",
path: "/ui/toolsets",
wantStatus: http.StatusOK,
wantContentType: "text/html",
wantPageTitle: "Toolsets View",
},
{
name: "web toolsets page with trailing slash",
path: "/ui/toolsets/",
wantStatus: http.StatusOK,
wantContentType: "text/html",
wantPageTitle: "Toolsets View",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
reqURL := ts.URL + tc.path
req, err := http.NewRequest(http.MethodGet, reqURL, nil)
if err != nil {
t.Fatalf("Failed to create request: %v", err)
}
client := ts.Client()
resp, err := client.Do(req)
if err != nil {
t.Fatalf("Failed to send request: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != tc.wantStatus {
body, _ := io.ReadAll(resp.Body)
t.Fatalf("Unexpected status code for %s: got %d, want %d, body: %s", tc.path, resp.StatusCode, tc.wantStatus, string(body))
}
contentType := resp.Header.Get("Content-Type")
if !strings.HasPrefix(contentType, tc.wantContentType) {
t.Errorf("Unexpected Content-Type header for %s: got %s, want prefix %s", tc.path, contentType, tc.wantContentType)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("Failed to read response body: %v", err)
}
doc, err := goquery.NewDocumentFromReader(strings.NewReader(string(body)))
if err != nil {
t.Fatalf("Failed to parse HTML: %v", err)
}
gotPageTitle := doc.Find("title").Text()
if gotPageTitle != tc.wantPageTitle {
t.Errorf("Unexpected page title for %s: got %q, want %q", tc.path, gotPageTitle, tc.wantPageTitle)
}
pageURL := resp.Request.URL
verifyLinkedResources(t, ts, pageURL, doc)
})
}
}
// verifyLinkedResources checks that resources linked in the HTML are served correctly.
func verifyLinkedResources(t *testing.T, ts *httptest.Server, pageURL *url.URL, doc *goquery.Document) {
t.Helper()
selectors := map[string]string{
"stylesheet": "link[rel=stylesheet]",
"script": "script[src]",
}
attrMap := map[string]string{
"stylesheet": "href",
"script": "src",
}
foundResource := false
for resourceType, selector := range selectors {
doc.Find(selector).Each(func(i int, s *goquery.Selection) {
foundResource = true
attrName := attrMap[resourceType]
resourcePath, exists := s.Attr(attrName)
if !exists || resourcePath == "" {
t.Errorf("Resource element %s is missing attribute %s on page %s", selector, attrName, pageURL.String())
return
}
// Resolve the URL relative to the page URL
resURL, err := url.Parse(resourcePath)
if err != nil {
t.Errorf("Failed to parse resource path %q on page %s: %v", resourcePath, pageURL.String(), err)
return
}
absoluteResourceURL := pageURL.ResolveReference(resURL)
// Skip external hosts
if absoluteResourceURL.Host != pageURL.Host {
t.Logf("Skipping resource on different host: %s", absoluteResourceURL.String())
return
}
resp, err := ts.Client().Get(absoluteResourceURL.String())
if err != nil {
t.Errorf("Failed to GET %s resource %s: %v", resourceType, absoluteResourceURL.String(), err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("Resource %s %s: expected status OK (200), but got %d", resourceType, absoluteResourceURL.String(), resp.StatusCode)
}
})
}
if !foundResource {
t.Logf("No stylesheet or script resources found to check on page %s", pageURL.String())
}
}

View File

@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package server
package telemetry
import (
"fmt"

View File

@@ -15,9 +15,12 @@
package testutils
import (
"bufio"
"context"
"fmt"
"io"
"os"
"regexp"
"strings"
"github.com/googleapis/genai-toolbox/internal/log"
@@ -42,3 +45,63 @@ func ContextWithNewLogger() (context.Context, error) {
}
return util.WithLogger(ctx, logger), nil
}
// WaitForString waits until the server logs a single line that matches the provided regex.
// returns the output of whatever the server sent so far.
func WaitForString(ctx context.Context, re *regexp.Regexp, pr io.ReadCloser) (string, error) {
in := bufio.NewReader(pr)
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// read lines in background, sending result of each read over a channel
// this allows us to use in.ReadString without blocking
type result struct {
s string
err error
}
output := make(chan result)
go func() {
defer close(output)
for {
select {
case <-ctx.Done():
// if the context is canceled, the orig thread will send back the error
// so we can just exit the goroutine here
return
default:
// otherwise read a line from the output
s, err := in.ReadString('\n')
if err != nil {
output <- result{err: err}
return
}
output <- result{s: s}
// if that last string matched, exit the goroutine
if re.MatchString(s) {
return
}
}
}
}()
// collect the output until the ctx is canceled, an error was hit,
// or match was found (which is indicated the channel is closed)
var sb strings.Builder
for {
select {
case <-ctx.Done():
// if ctx is done, return that error
return sb.String(), ctx.Err()
case o, ok := <-output:
if !ok {
// match was found!
return sb.String(), nil
}
if o.err != nil {
// error was found!
return sb.String(), o.err
}
sb.WriteString(o.s)
}
}
}

View File

@@ -26,6 +26,8 @@ import (
)
const kind string = "bigquery-get-dataset-info"
const projectKey string = "project"
const datasetKey string = "dataset"
func init() {
if !tools.Register(kind, newConfig) {
@@ -78,8 +80,9 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources)
}
datasetParameter := tools.NewStringParameter("dataset", "The dataset to get metadata information.")
parameters := tools.Parameters{datasetParameter}
projectParameter := tools.NewStringParameterWithDefault(projectKey, s.BigQueryClient().Project(), "The Google Cloud project ID containing the dataset.")
datasetParameter := tools.NewStringParameter(datasetKey, "The dataset to get metadata information.")
parameters := tools.Parameters{projectParameter, datasetParameter}
mcpManifest := tools.McpManifest{
Name: cfg.Name,
@@ -116,14 +119,18 @@ type Tool struct {
}
func (t Tool) Invoke(ctx context.Context, params tools.ParamValues) ([]any, error) {
sliceParams := params.AsSlice()
datasetId, ok := sliceParams[0].(string)
mapParams := params.AsMap()
projectId, ok := mapParams[projectKey].(string)
if !ok {
return nil, fmt.Errorf("unable to get cast %s", sliceParams[0])
return nil, fmt.Errorf("invalid or missing '%s' parameter; expected a string", projectKey)
}
dsHandle := t.Client.Dataset(datasetId)
datasetId, ok := mapParams[datasetKey].(string)
if !ok {
return nil, fmt.Errorf("invalid or missing '%s' parameter; expected a string", datasetKey)
}
dsHandle := t.Client.DatasetInProject(projectId, datasetId)
metadata, err := dsHandle.Metadata(ctx)
if err != nil {

View File

@@ -26,6 +26,9 @@ import (
)
const kind string = "bigquery-get-table-info"
const projectKey string = "project"
const datasetKey string = "dataset"
const tableKey string = "table"
func init() {
if !tools.Register(kind, newConfig) {
@@ -78,9 +81,10 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources)
}
datasetParameter := tools.NewStringParameter("dataset", "The table's parent dataset.")
tableParameter := tools.NewStringParameter("table", "The table to get metadata information.")
parameters := tools.Parameters{datasetParameter, tableParameter}
projectParameter := tools.NewStringParameterWithDefault(projectKey, s.BigQueryClient().Project(), "The Google Cloud project ID containing the dataset and table.")
datasetParameter := tools.NewStringParameter(datasetKey, "The table's parent dataset.")
tableParameter := tools.NewStringParameter(tableKey, "The table to get metadata information.")
parameters := tools.Parameters{projectParameter, datasetParameter, tableParameter}
mcpManifest := tools.McpManifest{
Name: cfg.Name,
@@ -117,22 +121,28 @@ type Tool struct {
}
func (t Tool) Invoke(ctx context.Context, params tools.ParamValues) ([]any, error) {
sliceParams := params.AsSlice()
datasetId, ok := sliceParams[0].(string)
mapParams := params.AsMap()
projectId, ok := mapParams[projectKey].(string)
if !ok {
return nil, fmt.Errorf("unable to get cast %s", sliceParams[0])
}
tableId, ok := sliceParams[1].(string)
if !ok {
return nil, fmt.Errorf("unable to get cast %s", sliceParams[1])
return nil, fmt.Errorf("invalid or missing '%s' parameter; expected a string", projectKey)
}
dsHandle := t.Client.Dataset(datasetId)
datasetId, ok := mapParams[datasetKey].(string)
if !ok {
return nil, fmt.Errorf("invalid or missing '%s' parameter; expected a string", datasetKey)
}
tableId, ok := mapParams[tableKey].(string)
if !ok {
return nil, fmt.Errorf("invalid or missing '%s' parameter; expected a string", tableKey)
}
dsHandle := t.Client.DatasetInProject(projectId, datasetId)
tableHandle := dsHandle.Table(tableId)
metadata, err := tableHandle.Metadata(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get metadata for table %s.%s.%s: %w", t.Client.Project(), datasetId, tableId, err)
return nil, fmt.Errorf("failed to get metadata for table %s.%s.%s: %w", projectId, datasetId, tableId, err)
}
return []any{metadata}, nil

View File

@@ -27,6 +27,7 @@ import (
)
const kind string = "bigquery-list-dataset-ids"
const projectKey string = "project"
func init() {
if !tools.Register(kind, newConfig) {
@@ -79,7 +80,9 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources)
}
parameters := tools.Parameters{}
projectParameter := tools.NewStringParameterWithDefault(projectKey, s.BigQueryClient().Project(), "The Google Cloud project to list dataset ids.")
parameters := tools.Parameters{projectParameter}
mcpManifest := tools.McpManifest{
Name: cfg.Name,
@@ -116,7 +119,13 @@ type Tool struct {
}
func (t Tool) Invoke(ctx context.Context, params tools.ParamValues) ([]any, error) {
mapParams := params.AsMap()
projectId, ok := mapParams[projectKey].(string)
if !ok {
return nil, fmt.Errorf("invalid or missing '%s' parameter; expected a string", projectKey)
}
datasetIterator := t.Client.Datasets(ctx)
datasetIterator.ProjectID = projectId
var datasetIds []any
for {

View File

@@ -27,6 +27,8 @@ import (
)
const kind string = "bigquery-list-table-ids"
const projectKey string = "project"
const datasetKey string = "dataset"
func init() {
if !tools.Register(kind, newConfig) {
@@ -79,8 +81,9 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
return nil, fmt.Errorf("invalid source for %q tool: source kind must be one of %q", kind, compatibleSources)
}
datasetParameter := tools.NewStringParameter("dataset", "The dataset to list table ids.")
parameters := tools.Parameters{datasetParameter}
projectParameter := tools.NewStringParameterWithDefault(projectKey, s.BigQueryClient().Project(), "The Google Cloud project ID containing the dataset.")
datasetParameter := tools.NewStringParameter(datasetKey, "The dataset to list table ids.")
parameters := tools.Parameters{projectParameter, datasetParameter}
mcpManifest := tools.McpManifest{
Name: cfg.Name,
@@ -117,14 +120,18 @@ type Tool struct {
}
func (t Tool) Invoke(ctx context.Context, params tools.ParamValues) ([]any, error) {
sliceParams := params.AsSlice()
datasetId, ok := sliceParams[0].(string)
mapParams := params.AsMap()
projectId, ok := mapParams[projectKey].(string)
if !ok {
return nil, fmt.Errorf("unable to get cast %s", sliceParams[0])
return nil, fmt.Errorf("invalid or missing '%s' parameter; expected a string", projectKey)
}
dsHandle := t.Client.Dataset(datasetId)
datasetId, ok := mapParams[datasetKey].(string)
if !ok {
return nil, fmt.Errorf("invalid or missing '%s' parameter; expected a string", datasetKey)
}
dsHandle := t.Client.DatasetInProject(projectId, datasetId)
var tableIds []any
tableIterator := dsHandle.Tables(ctx)

View File

@@ -319,13 +319,6 @@ func parseParamFromDelayedUnmarshaler(ctx context.Context, u *util.DelayedUnmars
if err := dec.DecodeContext(ctx, a); err != nil {
return nil, fmt.Errorf("unable to parse as %q: %w", t, err)
}
if a.Default != nil {
intD, ok := a.Default.(uint64)
if !ok {
return nil, fmt.Errorf("default value fail to assert to type int64")
}
a.Default = int(intD)
}
if a.AuthSources != nil {
logger.WarnContext(ctx, "`authSources` is deprecated, use `authServices` for parameters instead")
a.AuthServices = append(a.AuthServices, a.AuthSources...)
@@ -418,7 +411,6 @@ type ParameterMcpManifest struct {
type CommonParameter struct {
Name string `yaml:"name" validate:"required"`
Type string `yaml:"type" validate:"required"`
Default any `yaml:"default"`
Desc string `yaml:"description" validate:"required"`
AuthServices []ParamAuthService `yaml:"authServices"`
AuthSources []ParamAuthService `yaml:"authSources"` // Deprecated: Kept for compatibility.
@@ -434,30 +426,6 @@ func (p *CommonParameter) GetType() string {
return p.Type
}
func (p *CommonParameter) GetDefault() any {
return p.Default
}
// Manifest returns the manifest for the Parameter.
func (p *CommonParameter) Manifest() ParameterManifest {
// only list ParamAuthService names (without fields) in manifest
authNames := make([]string, len(p.AuthServices))
for i, a := range p.AuthServices {
authNames[i] = a.Name
}
var required bool
if p.Default == nil {
required = true
}
return ParameterManifest{
Name: p.Name,
Type: p.Type,
Required: required,
Description: p.Desc,
AuthServices: authNames,
}
}
// McpManifest returns the MCP manifest for the Parameter.
func (p *CommonParameter) McpManifest() ParameterMcpManifest {
return ParameterMcpManifest{
@@ -500,10 +468,10 @@ func NewStringParameterWithDefault(name string, defaultV, desc string) *StringPa
CommonParameter: CommonParameter{
Name: name,
Type: typeString,
Default: defaultV,
Desc: desc,
AuthServices: nil,
},
Default: &defaultV,
}
}
@@ -524,6 +492,7 @@ var _ Parameter = &StringParameter{}
// StringParameter is a parameter representing the "string" type.
type StringParameter struct {
CommonParameter `yaml:",inline"`
Default *string `yaml:"default"`
}
// Parse casts the value "v" as a "string".
@@ -534,10 +503,35 @@ func (p *StringParameter) Parse(v any) (any, error) {
}
return newV, nil
}
func (p *StringParameter) GetAuthServices() []ParamAuthService {
return p.AuthServices
}
func (p *StringParameter) GetDefault() any {
if p.Default == nil {
return nil
}
return *p.Default
}
// Manifest returns the manifest for the StringParameter.
func (p *StringParameter) Manifest() ParameterManifest {
// only list ParamAuthService names (without fields) in manifest
authNames := make([]string, len(p.AuthServices))
for i, a := range p.AuthServices {
authNames[i] = a.Name
}
required := p.Default == nil
return ParameterManifest{
Name: p.Name,
Type: p.Type,
Required: required,
Description: p.Desc,
AuthServices: authNames,
}
}
// NewIntParameter is a convenience function for initializing a IntParameter.
func NewIntParameter(name string, desc string) *IntParameter {
return &IntParameter{
@@ -551,15 +545,15 @@ func NewIntParameter(name string, desc string) *IntParameter {
}
// NewIntParameterWithDefault is a convenience function for initializing a IntParameter with default value.
func NewIntParameterWithDefault(name string, defaultV any, desc string) *IntParameter {
func NewIntParameterWithDefault(name string, defaultV int, desc string) *IntParameter {
return &IntParameter{
CommonParameter: CommonParameter{
Name: name,
Type: typeInt,
Default: defaultV,
Desc: desc,
AuthServices: nil,
},
Default: &defaultV,
}
}
@@ -580,6 +574,7 @@ var _ Parameter = &IntParameter{}
// IntParameter is a parameter representing the "int" type.
type IntParameter struct {
CommonParameter `yaml:",inline"`
Default *int `yaml:"default"`
}
func (p *IntParameter) Parse(v any) (any, error) {
@@ -607,6 +602,30 @@ func (p *IntParameter) GetAuthServices() []ParamAuthService {
return p.AuthServices
}
func (p *IntParameter) GetDefault() any {
if p.Default == nil {
return nil
}
return *p.Default
}
// Manifest returns the manifest for the IntParameter.
func (p *IntParameter) Manifest() ParameterManifest {
// only list ParamAuthService names (without fields) in manifest
authNames := make([]string, len(p.AuthServices))
for i, a := range p.AuthServices {
authNames[i] = a.Name
}
required := p.Default == nil
return ParameterManifest{
Name: p.Name,
Type: p.Type,
Required: required,
Description: p.Desc,
AuthServices: authNames,
}
}
// NewFloatParameter is a convenience function for initializing a FloatParameter.
func NewFloatParameter(name string, desc string) *FloatParameter {
return &FloatParameter{
@@ -625,10 +644,10 @@ func NewFloatParameterWithDefault(name string, defaultV float64, desc string) *F
CommonParameter: CommonParameter{
Name: name,
Type: typeFloat,
Default: defaultV,
Desc: desc,
AuthServices: nil,
},
Default: &defaultV,
}
}
@@ -649,6 +668,7 @@ var _ Parameter = &FloatParameter{}
// FloatParameter is a parameter representing the "float" type.
type FloatParameter struct {
CommonParameter `yaml:",inline"`
Default *float64 `yaml:"default"`
}
func (p *FloatParameter) Parse(v any) (any, error) {
@@ -674,6 +694,30 @@ func (p *FloatParameter) GetAuthServices() []ParamAuthService {
return p.AuthServices
}
func (p *FloatParameter) GetDefault() any {
if p.Default == nil {
return nil
}
return *p.Default
}
// Manifest returns the manifest for the FloatParameter.
func (p *FloatParameter) Manifest() ParameterManifest {
// only list ParamAuthService names (without fields) in manifest
authNames := make([]string, len(p.AuthServices))
for i, a := range p.AuthServices {
authNames[i] = a.Name
}
required := p.Default == nil
return ParameterManifest{
Name: p.Name,
Type: p.Type,
Required: required,
Description: p.Desc,
AuthServices: authNames,
}
}
// NewBooleanParameter is a convenience function for initializing a BooleanParameter.
func NewBooleanParameter(name string, desc string) *BooleanParameter {
return &BooleanParameter{
@@ -692,10 +736,10 @@ func NewBooleanParameterWithDefault(name string, defaultV bool, desc string) *Bo
CommonParameter: CommonParameter{
Name: name,
Type: typeBool,
Default: defaultV,
Desc: desc,
AuthServices: nil,
},
Default: &defaultV,
}
}
@@ -716,6 +760,7 @@ var _ Parameter = &BooleanParameter{}
// BooleanParameter is a parameter representing the "boolean" type.
type BooleanParameter struct {
CommonParameter `yaml:",inline"`
Default *bool `yaml:"default"`
}
func (p *BooleanParameter) Parse(v any) (any, error) {
@@ -730,6 +775,30 @@ func (p *BooleanParameter) GetAuthServices() []ParamAuthService {
return p.AuthServices
}
func (p *BooleanParameter) GetDefault() any {
if p.Default == nil {
return nil
}
return *p.Default
}
// Manifest returns the manifest for the BooleanParameter.
func (p *BooleanParameter) Manifest() ParameterManifest {
// only list ParamAuthService names (without fields) in manifest
authNames := make([]string, len(p.AuthServices))
for i, a := range p.AuthServices {
authNames[i] = a.Name
}
required := p.Default == nil
return ParameterManifest{
Name: p.Name,
Type: p.Type,
Required: required,
Description: p.Desc,
AuthServices: authNames,
}
}
// NewArrayParameter is a convenience function for initializing a ArrayParameter.
func NewArrayParameter(name string, desc string, items Parameter) *ArrayParameter {
return &ArrayParameter{
@@ -744,16 +813,16 @@ func NewArrayParameter(name string, desc string, items Parameter) *ArrayParamete
}
// NewArrayParameterWithDefault is a convenience function for initializing a ArrayParameter with default value.
func NewArrayParameterWithDefault(name string, defaultV any, desc string, items Parameter) *ArrayParameter {
func NewArrayParameterWithDefault(name string, defaultV []any, desc string, items Parameter) *ArrayParameter {
return &ArrayParameter{
CommonParameter: CommonParameter{
Name: name,
Type: typeArray,
Default: defaultV,
Desc: desc,
AuthServices: nil,
},
Items: items,
Items: items,
Default: &defaultV,
}
}
@@ -775,18 +844,21 @@ var _ Parameter = &ArrayParameter{}
// ArrayParameter is a parameter representing the "array" type.
type ArrayParameter struct {
CommonParameter `yaml:",inline"`
Default *[]any `yaml:"default"`
Items Parameter `yaml:"items"`
}
func (p *ArrayParameter) UnmarshalYAML(ctx context.Context, unmarshal func(interface{}) error) error {
var rawItem struct {
CommonParameter `yaml:",inline"`
Default *[]any `yaml:"default"`
Items util.DelayedUnmarshaler `yaml:"items"`
}
if err := unmarshal(&rawItem); err != nil {
return err
}
p.CommonParameter = rawItem.CommonParameter
p.Default = rawItem.Default
i, err := parseParamFromDelayedUnmarshaler(ctx, &rawItem.Items)
if err != nil {
return fmt.Errorf("unable to parse 'items' field: %w", err)
@@ -819,6 +891,13 @@ func (p *ArrayParameter) GetAuthServices() []ParamAuthService {
return p.AuthServices
}
func (p *ArrayParameter) GetDefault() any {
if p.Default == nil {
return nil
}
return *p.Default
}
// Manifest returns the manifest for the ArrayParameter.
func (p *ArrayParameter) Manifest() ParameterManifest {
// only list ParamAuthService names (without fields) in manifest
@@ -827,11 +906,8 @@ func (p *ArrayParameter) Manifest() ParameterManifest {
authNames[i] = a.Name
}
items := p.Items.Manifest()
var required bool
if p.Default == nil {
required = true
items.Required = true
}
required := p.Default == nil
items.Required = required
return ParameterManifest{
Name: p.Name,
Type: p.Type,

View File

@@ -187,7 +187,7 @@ func TestParametersMarshal(t *testing.T) {
{
"name": "my_array",
"type": "array",
"default": `["foo", "bar"]`,
"default": []any{"foo", "bar"},
"description": "this param is an array of strings",
"items": map[string]string{
"name": "my_string",
@@ -197,7 +197,7 @@ func TestParametersMarshal(t *testing.T) {
},
},
want: tools.Parameters{
tools.NewArrayParameterWithDefault("my_array", `["foo", "bar"]`, "this param is an array of strings", tools.NewStringParameter("my_string", "string item")),
tools.NewArrayParameterWithDefault("my_array", []any{"foo", "bar"}, "this param is an array of strings", tools.NewStringParameter("my_string", "string item")),
},
},
{
@@ -206,7 +206,7 @@ func TestParametersMarshal(t *testing.T) {
{
"name": "my_array",
"type": "array",
"default": "[1.0, 1.1]",
"default": []any{1.0, 1.1},
"description": "this param is an array of floats",
"items": map[string]string{
"name": "my_float",
@@ -216,7 +216,7 @@ func TestParametersMarshal(t *testing.T) {
},
},
want: tools.Parameters{
tools.NewArrayParameterWithDefault("my_array", "[1.0, 1.1]", "this param is an array of floats", tools.NewFloatParameter("my_float", "float item")),
tools.NewArrayParameterWithDefault("my_array", []any{1.0, 1.1}, "this param is an array of floats", tools.NewFloatParameter("my_float", "float item")),
},
},
}
@@ -993,14 +993,14 @@ func TestParamManifest(t *testing.T) {
},
{
name: "array default",
in: tools.NewArrayParameterWithDefault("foo-array", `["foo", "bar"]`, "bar", tools.NewStringParameter("foo-string", "bar")),
in: tools.NewArrayParameterWithDefault("foo-array", []any{"foo", "bar"}, "bar", tools.NewStringParameter("foo-string", "bar")),
want: tools.ParameterManifest{
Name: "foo-array",
Type: "array",
Required: false,
Description: "bar",
AuthServices: []string{},
Items: &tools.ParameterManifest{Name: "foo-string", Type: "string", Required: true, Description: "bar", AuthServices: []string{}},
Items: &tools.ParameterManifest{Name: "foo-string", Type: "string", Required: false, Description: "bar", AuthServices: []string{}},
},
},
}
@@ -1073,7 +1073,7 @@ func TestMcpManifest(t *testing.T) {
tools.NewStringParameter("foo-string2", "bar"),
tools.NewIntParameterWithDefault("foo-int", 1, "bar"),
tools.NewIntParameter("foo-int2", "bar"),
tools.NewArrayParameterWithDefault("foo-array", []string{"hello", "world"}, "bar", tools.NewStringParameter("foo-string", "bar")),
tools.NewArrayParameterWithDefault("foo-array", []any{"hello", "world"}, "bar", tools.NewStringParameter("foo-string", "bar")),
tools.NewArrayParameter("foo-array2", "bar", tools.NewStringParameter("foo-string", "bar")),
},
want: tools.McpToolsSchema{

View File

@@ -23,6 +23,7 @@ import (
"github.com/go-playground/validator/v10"
yaml "github.com/goccy/go-yaml"
"github.com/googleapis/genai-toolbox/internal/log"
"github.com/googleapis/genai-toolbox/internal/telemetry"
)
// DecodeJSON decodes a given reader into an interface using the json decoder.
@@ -98,10 +99,25 @@ func WithLogger(ctx context.Context, logger log.Logger) context.Context {
return context.WithValue(ctx, loggerKey, logger)
}
// LoggerFromContext retreives the logger or return an error
// LoggerFromContext retrieves the logger or return an error
func LoggerFromContext(ctx context.Context) (log.Logger, error) {
if logger, ok := ctx.Value(loggerKey).(log.Logger); ok {
return logger, nil
}
return nil, fmt.Errorf("unable to retrieve logger")
}
const instrumentationKey contextKey = "instrumentation"
// WithInstrumentation adds an instrumentation into the context as a value
func WithInstrumentation(ctx context.Context, instrumentation *telemetry.Instrumentation) context.Context {
return context.WithValue(ctx, instrumentationKey, instrumentation)
}
// InstrumentationFromContext retrieves the instrumentation or return an error
func InstrumentationFromContext(ctx context.Context) (*telemetry.Instrumentation, error) {
if instrumentation, ok := ctx.Value(instrumentationKey).(*telemetry.Instrumentation); ok {
return instrumentation, nil
}
return nil, fmt.Errorf("unable to retrieve instrumentation")
}

View File

@@ -28,59 +28,60 @@ import (
"time"
"github.com/googleapis/genai-toolbox/internal/server/mcp/jsonrpc"
"github.com/googleapis/genai-toolbox/internal/testutils"
"github.com/googleapis/genai-toolbox/tests"
)
var (
ALLOYDB_AI_NL_SOURCE_KIND = "alloydb-postgres"
ALLOYDB_AI_NL_TOOL_KIND = "alloydb-ai-nl"
ALLOYDB_AI_NL_PROJECT = os.Getenv("ALLOYDB_AI_NL_PROJECT")
ALLOYDB_AI_NL_REGION = os.Getenv("ALLOYDB_AI_NL_REGION")
ALLOYDB_AI_NL_CLUSTER = os.Getenv("ALLOYDB_AI_NL_CLUSTER")
ALLOYDB_AI_NL_INSTANCE = os.Getenv("ALLOYDB_AI_NL_INSTANCE")
ALLOYDB_AI_NL_DATABASE = os.Getenv("ALLOYDB_AI_NL_DATABASE")
ALLOYDB_AI_NL_USER = os.Getenv("ALLOYDB_AI_NL_USER")
ALLOYDB_AI_NL_PASS = os.Getenv("ALLOYDB_AI_NL_PASS")
AlloyDBAINLSourceKind = "alloydb-postgres"
AlloyDBAINLToolKind = "alloydb-ai-nl"
AlloyDBAINLProject = os.Getenv("ALLOYDB_AI_NL_PROJECT")
AlloyDBAINLRegion = os.Getenv("ALLOYDB_AI_NL_REGION")
AlloyDBAINLCluster = os.Getenv("ALLOYDB_AI_NL_CLUSTER")
AlloyDBAINLInstance = os.Getenv("ALLOYDB_AI_NL_INSTANCE")
AlloyDBAINLDatabase = os.Getenv("ALLOYDB_AI_NL_DATABASE")
AlloyDBAINLUser = os.Getenv("ALLOYDB_AI_NL_USER")
AlloyDBAINLPass = os.Getenv("ALLOYDB_AI_NL_PASS")
)
func getAlloyDBAiNlVars(t *testing.T) map[string]any {
func getAlloyDBAINLVars(t *testing.T) map[string]any {
switch "" {
case ALLOYDB_AI_NL_PROJECT:
case AlloyDBAINLProject:
t.Fatal("'ALLOYDB_AI_NL_PROJECT' not set")
case ALLOYDB_AI_NL_REGION:
case AlloyDBAINLRegion:
t.Fatal("'ALLOYDB_AI_NL_REGION' not set")
case ALLOYDB_AI_NL_CLUSTER:
case AlloyDBAINLCluster:
t.Fatal("'ALLOYDB_AI_NL_CLUSTER' not set")
case ALLOYDB_AI_NL_INSTANCE:
case AlloyDBAINLInstance:
t.Fatal("'ALLOYDB_AI_NL_INSTANCE' not set")
case ALLOYDB_AI_NL_DATABASE:
case AlloyDBAINLDatabase:
t.Fatal("'ALLOYDB_AI_NL_DATABASE' not set")
case ALLOYDB_AI_NL_USER:
case AlloyDBAINLUser:
t.Fatal("'ALLOYDB_AI_NL_USER' not set")
case ALLOYDB_AI_NL_PASS:
case AlloyDBAINLPass:
t.Fatal("'ALLOYDB_AI_NL_PASS' not set")
}
return map[string]any{
"kind": ALLOYDB_AI_NL_SOURCE_KIND,
"project": ALLOYDB_AI_NL_PROJECT,
"cluster": ALLOYDB_AI_NL_CLUSTER,
"instance": ALLOYDB_AI_NL_INSTANCE,
"region": ALLOYDB_AI_NL_REGION,
"database": ALLOYDB_AI_NL_DATABASE,
"user": ALLOYDB_AI_NL_USER,
"password": ALLOYDB_AI_NL_PASS,
"kind": AlloyDBAINLSourceKind,
"project": AlloyDBAINLProject,
"cluster": AlloyDBAINLCluster,
"instance": AlloyDBAINLInstance,
"region": AlloyDBAINLRegion,
"database": AlloyDBAINLDatabase,
"user": AlloyDBAINLUser,
"password": AlloyDBAINLPass,
}
}
func TestAlloyDBAiNlToolEndpoints(t *testing.T) {
sourceConfig := getAlloyDBAiNlVars(t)
func TestAlloyDBAINLToolEndpoints(t *testing.T) {
sourceConfig := getAlloyDBAINLVars(t)
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
var args []string
// Write config into a file and pass it to command
toolsFile := getAiNlToolsConfig(sourceConfig)
toolsFile := getAINLToolsConfig(sourceConfig)
cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...)
if err != nil {
@@ -90,18 +91,18 @@ func TestAlloyDBAiNlToolEndpoints(t *testing.T) {
waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
out, err := cmd.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`))
out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out)
if err != nil {
t.Logf("toolbox command logs: \n%s", out)
t.Fatalf("toolbox didn't start successfully: %s", err)
}
runAiNlToolGetTest(t)
runAiNlToolInvokeTest(t)
runAiNlMCPToolCallMethod(t)
runAINLToolGetTest(t)
runAINLToolInvokeTest(t)
runAINLMCPToolCallMethod(t)
}
func runAiNlToolGetTest(t *testing.T) {
func runAINLToolGetTest(t *testing.T) {
// Test tool get endpoint
tcs := []struct {
name string
@@ -156,7 +157,7 @@ func runAiNlToolGetTest(t *testing.T) {
}
}
func runAiNlToolInvokeTest(t *testing.T) {
func runAINLToolInvokeTest(t *testing.T) {
// Get ID token
idToken, err := tests.GetGoogleIdToken(tests.ClientId)
if err != nil {
@@ -276,7 +277,7 @@ func runAiNlToolInvokeTest(t *testing.T) {
}
func getAiNlToolsConfig(sourceConfig map[string]any) map[string]any {
func getAINLToolsConfig(sourceConfig map[string]any) map[string]any {
// Write config into a file and pass it to command
toolsFile := map[string]any{
"sources": map[string]any{
@@ -290,13 +291,13 @@ func getAiNlToolsConfig(sourceConfig map[string]any) map[string]any {
},
"tools": map[string]any{
"my-simple-tool": map[string]any{
"kind": ALLOYDB_AI_NL_TOOL_KIND,
"kind": AlloyDBAINLToolKind,
"source": "my-instance",
"description": "Simple tool to test end to end functionality.",
"nlConfig": "my_nl_config",
},
"my-auth-tool": map[string]any{
"kind": ALLOYDB_AI_NL_TOOL_KIND,
"kind": AlloyDBAINLToolKind,
"source": "my-instance",
"description": "Tool to test authenticated parameters.",
"nlConfig": "my_nl_config",
@@ -315,7 +316,7 @@ func getAiNlToolsConfig(sourceConfig map[string]any) map[string]any {
},
},
"my-auth-required-tool": map[string]any{
"kind": ALLOYDB_AI_NL_TOOL_KIND,
"kind": AlloyDBAINLToolKind,
"source": "my-instance",
"description": "Tool to test auth required invocation.",
"nlConfig": "my_nl_config",
@@ -329,7 +330,7 @@ func getAiNlToolsConfig(sourceConfig map[string]any) map[string]any {
return toolsFile
}
func runAiNlMCPToolCallMethod(t *testing.T) {
func runAINLMCPToolCallMethod(t *testing.T) {
sessionId := tests.RunInitialize(t, "2024-11-05")
header := map[string]string{}
if sessionId != "" {

View File

@@ -26,65 +26,66 @@ import (
"cloud.google.com/go/alloydbconn"
"github.com/google/uuid"
"github.com/googleapis/genai-toolbox/internal/testutils"
"github.com/googleapis/genai-toolbox/tests"
"github.com/jackc/pgx/v5/pgxpool"
)
var (
ALLOYDB_POSTGRES_SOURCE_KIND = "alloydb-postgres"
ALLOYDB_POSTGRES_TOOL_KIND = "postgres-sql"
ALLOYDB_POSTGRES_PROJECT = os.Getenv("ALLOYDB_POSTGRES_PROJECT")
ALLOYDB_POSTGRES_REGION = os.Getenv("ALLOYDB_POSTGRES_REGION")
ALLOYDB_POSTGRES_CLUSTER = os.Getenv("ALLOYDB_POSTGRES_CLUSTER")
ALLOYDB_POSTGRES_INSTANCE = os.Getenv("ALLOYDB_POSTGRES_INSTANCE")
ALLOYDB_POSTGRES_DATABASE = os.Getenv("ALLOYDB_POSTGRES_DATABASE")
ALLOYDB_POSTGRES_USER = os.Getenv("ALLOYDB_POSTGRES_USER")
ALLOYDB_POSTGRES_PASS = os.Getenv("ALLOYDB_POSTGRES_PASS")
AlloyDBPostgresSourceKind = "alloydb-postgres"
AlloyDBPostgresToolKind = "postgres-sql"
AlloyDBPostgresProject = os.Getenv("ALLOYDB_POSTGRES_PROJECT")
AlloyDBPostgresRegion = os.Getenv("ALLOYDB_POSTGRES_REGION")
AlloyDBPostgresCluster = os.Getenv("ALLOYDB_POSTGRES_CLUSTER")
AlloyDBPostgresInstance = os.Getenv("ALLOYDB_POSTGRES_INSTANCE")
AlloyDBPostgresDatabase = os.Getenv("ALLOYDB_POSTGRES_DATABASE")
AlloyDBPostgresUser = os.Getenv("ALLOYDB_POSTGRES_USER")
AlloyDBPostgresPass = os.Getenv("ALLOYDB_POSTGRES_PASS")
)
func getAlloyDBPgVars(t *testing.T) map[string]any {
switch "" {
case ALLOYDB_POSTGRES_PROJECT:
case AlloyDBPostgresProject:
t.Fatal("'ALLOYDB_POSTGRES_PROJECT' not set")
case ALLOYDB_POSTGRES_REGION:
case AlloyDBPostgresRegion:
t.Fatal("'ALLOYDB_POSTGRES_REGION' not set")
case ALLOYDB_POSTGRES_CLUSTER:
case AlloyDBPostgresCluster:
t.Fatal("'ALLOYDB_POSTGRES_CLUSTER' not set")
case ALLOYDB_POSTGRES_INSTANCE:
case AlloyDBPostgresInstance:
t.Fatal("'ALLOYDB_POSTGRES_INSTANCE' not set")
case ALLOYDB_POSTGRES_DATABASE:
case AlloyDBPostgresDatabase:
t.Fatal("'ALLOYDB_POSTGRES_DATABASE' not set")
case ALLOYDB_POSTGRES_USER:
case AlloyDBPostgresUser:
t.Fatal("'ALLOYDB_POSTGRES_USER' not set")
case ALLOYDB_POSTGRES_PASS:
case AlloyDBPostgresPass:
t.Fatal("'ALLOYDB_POSTGRES_PASS' not set")
}
return map[string]any{
"kind": ALLOYDB_POSTGRES_SOURCE_KIND,
"project": ALLOYDB_POSTGRES_PROJECT,
"cluster": ALLOYDB_POSTGRES_CLUSTER,
"instance": ALLOYDB_POSTGRES_INSTANCE,
"region": ALLOYDB_POSTGRES_REGION,
"database": ALLOYDB_POSTGRES_DATABASE,
"user": ALLOYDB_POSTGRES_USER,
"password": ALLOYDB_POSTGRES_PASS,
"kind": AlloyDBPostgresSourceKind,
"project": AlloyDBPostgresProject,
"cluster": AlloyDBPostgresCluster,
"instance": AlloyDBPostgresInstance,
"region": AlloyDBPostgresRegion,
"database": AlloyDBPostgresDatabase,
"user": AlloyDBPostgresUser,
"password": AlloyDBPostgresPass,
}
}
// Copied over from alloydb_pg.go
func getAlloyDBDialOpts(ip_type string) ([]alloydbconn.DialOption, error) {
switch strings.ToLower(ip_type) {
func getAlloyDBDialOpts(ipType string) ([]alloydbconn.DialOption, error) {
switch strings.ToLower(ipType) {
case "private":
return []alloydbconn.DialOption{alloydbconn.WithPrivateIP()}, nil
case "public":
return []alloydbconn.DialOption{alloydbconn.WithPublicIP()}, nil
default:
return nil, fmt.Errorf("invalid ip_type %s", ip_type)
return nil, fmt.Errorf("invalid ipType %s", ipType)
}
}
// Copied over from alloydb_pg.go
func initAlloyDBPgConnectionPool(project, region, cluster, instance, ip_type, user, pass, dbname string) (*pgxpool.Pool, error) {
func initAlloyDBPgConnectionPool(project, region, cluster, instance, ipType, user, pass, dbname string) (*pgxpool.Pool, error) {
// Configure the driver to connect to the database
dsn := fmt.Sprintf("user=%s password=%s dbname=%s sslmode=disable", user, pass, dbname)
config, err := pgxpool.ParseConfig(dsn)
@@ -93,7 +94,7 @@ func initAlloyDBPgConnectionPool(project, region, cluster, instance, ip_type, us
}
// Create a new dialer with options
dialOpts, err := getAlloyDBDialOpts(ip_type)
dialOpts, err := getAlloyDBDialOpts(ipType)
if err != nil {
return nil, err
}
@@ -123,7 +124,7 @@ func TestAlloyDBPgToolEndpoints(t *testing.T) {
var args []string
pool, err := initAlloyDBPgConnectionPool(ALLOYDB_POSTGRES_PROJECT, ALLOYDB_POSTGRES_REGION, ALLOYDB_POSTGRES_CLUSTER, ALLOYDB_POSTGRES_INSTANCE, "public", ALLOYDB_POSTGRES_USER, ALLOYDB_POSTGRES_PASS, ALLOYDB_POSTGRES_DATABASE)
pool, err := initAlloyDBPgConnectionPool(AlloyDBPostgresProject, AlloyDBPostgresRegion, AlloyDBPostgresCluster, AlloyDBPostgresInstance, "public", AlloyDBPostgresUser, AlloyDBPostgresPass, AlloyDBPostgresDatabase)
if err != nil {
t.Fatalf("unable to create AlloyDB connection pool: %s", err)
}
@@ -134,20 +135,20 @@ func TestAlloyDBPgToolEndpoints(t *testing.T) {
tableNameTemplateParam := "template_param_table_" + strings.ReplaceAll(uuid.New().String(), "-", "")
// set up data for param tool
create_statement1, insert_statement1, tool_statement1, params1 := tests.GetPostgresSQLParamToolInfo(tableNameParam)
teardownTable1 := tests.SetupPostgresSQLTable(t, ctx, pool, create_statement1, insert_statement1, tableNameParam, params1)
createStatement1, insertStatement1, paramToolStatement1, paramToolStatement2, params1 := tests.GetPostgresSQLParamToolInfo(tableNameParam)
teardownTable1 := tests.SetupPostgresSQLTable(t, ctx, pool, createStatement1, insertStatement1, tableNameParam, params1)
defer teardownTable1(t)
// set up data for auth tool
create_statement2, insert_statement2, tool_statement2, params2 := tests.GetPostgresSQLAuthToolInfo(tableNameAuth)
teardownTable2 := tests.SetupPostgresSQLTable(t, ctx, pool, create_statement2, insert_statement2, tableNameAuth, params2)
createStatement2, insertStatement2, authToolStatement, params2 := tests.GetPostgresSQLAuthToolInfo(tableNameAuth)
teardownTable2 := tests.SetupPostgresSQLTable(t, ctx, pool, createStatement2, insertStatement2, tableNameAuth, params2)
defer teardownTable2(t)
// Write config into a file and pass it to command
toolsFile := tests.GetToolsConfig(sourceConfig, ALLOYDB_POSTGRES_TOOL_KIND, tool_statement1, tool_statement2)
toolsFile := tests.GetToolsConfig(sourceConfig, AlloyDBPostgresToolKind, paramToolStatement1, paramToolStatement2, authToolStatement)
toolsFile = tests.AddPgExecuteSqlConfig(t, toolsFile)
tmplSelectCombined, tmplSelectFilterCombined := tests.GetPostgresSQLTmplToolStatement()
toolsFile = tests.AddTemplateParamConfig(t, toolsFile, ALLOYDB_POSTGRES_TOOL_KIND, tmplSelectCombined, tmplSelectFilterCombined, "")
toolsFile = tests.AddTemplateParamConfig(t, toolsFile, AlloyDBPostgresToolKind, tmplSelectCombined, tmplSelectFilterCombined, "")
cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...)
if err != nil {
@@ -157,7 +158,7 @@ func TestAlloyDBPgToolEndpoints(t *testing.T) {
waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
out, err := cmd.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`))
out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out)
if err != nil {
t.Logf("toolbox command logs: \n%s", out)
t.Fatalf("toolbox didn't start successfully: %s", err)
@@ -166,8 +167,8 @@ func TestAlloyDBPgToolEndpoints(t *testing.T) {
tests.RunToolGetTest(t)
select1Want, failInvocationWant, createTableStatement := tests.GetPostgresWants()
invokeParamWant, mcpInvokeParamWant := tests.GetNonSpannerInvokeParamWant()
tests.RunToolInvokeTest(t, select1Want, invokeParamWant)
invokeParamWant, invokeParamWantNull, mcpInvokeParamWant := tests.GetNonSpannerInvokeParamWant()
tests.RunToolInvokeTest(t, select1Want, invokeParamWant, invokeParamWantNull)
tests.RunExecuteSqlToolInvokeTest(t, createTableStatement, select1Want)
tests.RunMCPToolCallMethod(t, mcpInvokeParamWant, failInvocationWant)
tests.RunToolInvokeWithTemplateParameters(t, tableNameTemplateParam, tests.NewTemplateParameterTestConfig())
@@ -193,7 +194,7 @@ func TestAlloyDBPgIpConnection(t *testing.T) {
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
sourceConfig["ipType"] = tc.ipType
err := tests.RunSourceConnectionTest(t, sourceConfig, ALLOYDB_POSTGRES_TOOL_KIND)
err := tests.RunSourceConnectionTest(t, sourceConfig, AlloyDBPostgresToolKind)
if err != nil {
t.Fatalf("Connection test failure: %s", err)
}
@@ -205,35 +206,35 @@ func TestAlloyDBPgIpConnection(t *testing.T) {
func TestAlloyDBPgIAMConnection(t *testing.T) {
getAlloyDBPgVars(t)
// service account email used for IAM should trim the suffix
serviceAccountEmail := strings.TrimSuffix(tests.SERVICE_ACCOUNT_EMAIL, ".gserviceaccount.com")
serviceAccountEmail := strings.TrimSuffix(tests.ServiceAccountEmail, ".gserviceaccount.com")
noPassSourceConfig := map[string]any{
"kind": ALLOYDB_POSTGRES_SOURCE_KIND,
"project": ALLOYDB_POSTGRES_PROJECT,
"cluster": ALLOYDB_POSTGRES_CLUSTER,
"instance": ALLOYDB_POSTGRES_INSTANCE,
"region": ALLOYDB_POSTGRES_REGION,
"database": ALLOYDB_POSTGRES_DATABASE,
"kind": AlloyDBPostgresSourceKind,
"project": AlloyDBPostgresProject,
"cluster": AlloyDBPostgresCluster,
"instance": AlloyDBPostgresInstance,
"region": AlloyDBPostgresRegion,
"database": AlloyDBPostgresDatabase,
"user": serviceAccountEmail,
}
noUserSourceConfig := map[string]any{
"kind": ALLOYDB_POSTGRES_SOURCE_KIND,
"project": ALLOYDB_POSTGRES_PROJECT,
"cluster": ALLOYDB_POSTGRES_CLUSTER,
"instance": ALLOYDB_POSTGRES_INSTANCE,
"region": ALLOYDB_POSTGRES_REGION,
"database": ALLOYDB_POSTGRES_DATABASE,
"kind": AlloyDBPostgresSourceKind,
"project": AlloyDBPostgresProject,
"cluster": AlloyDBPostgresCluster,
"instance": AlloyDBPostgresInstance,
"region": AlloyDBPostgresRegion,
"database": AlloyDBPostgresDatabase,
"password": "random",
}
noUserNoPassSourceConfig := map[string]any{
"kind": ALLOYDB_POSTGRES_SOURCE_KIND,
"project": ALLOYDB_POSTGRES_PROJECT,
"cluster": ALLOYDB_POSTGRES_CLUSTER,
"instance": ALLOYDB_POSTGRES_INSTANCE,
"region": ALLOYDB_POSTGRES_REGION,
"database": ALLOYDB_POSTGRES_DATABASE,
"kind": AlloyDBPostgresSourceKind,
"project": AlloyDBPostgresProject,
"cluster": AlloyDBPostgresCluster,
"instance": AlloyDBPostgresInstance,
"region": AlloyDBPostgresRegion,
"database": AlloyDBPostgresDatabase,
}
tcs := []struct {
name string
@@ -258,7 +259,7 @@ func TestAlloyDBPgIAMConnection(t *testing.T) {
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
err := tests.RunSourceConnectionTest(t, tc.sourceConfig, ALLOYDB_POSTGRES_TOOL_KIND)
err := tests.RunSourceConnectionTest(t, tc.sourceConfig, AlloyDBPostgresToolKind)
if err != nil {
if tc.isErr {
return

View File

@@ -23,7 +23,7 @@ import (
"google.golang.org/api/idtoken"
)
var SERVICE_ACCOUNT_EMAIL = os.Getenv("SERVICE_ACCOUNT_EMAIL")
var ServiceAccountEmail = os.Getenv("SERVICE_ACCOUNT_EMAIL")
var ClientId = os.Getenv("CLIENT_ID")
// GetGoogleIdToken retrieve and return the Google ID token

View File

@@ -29,6 +29,7 @@ import (
bigqueryapi "cloud.google.com/go/bigquery"
"github.com/google/uuid"
"github.com/googleapis/genai-toolbox/internal/testutils"
"github.com/googleapis/genai-toolbox/tests"
"golang.org/x/oauth2/google"
"google.golang.org/api/googleapi"
@@ -37,20 +38,20 @@ import (
)
var (
BIGQUERY_SOURCE_KIND = "bigquery"
BIGQUERY_TOOL_KIND = "bigquery-sql"
BIGQUERY_PROJECT = os.Getenv("BIGQUERY_PROJECT")
BigquerySourceKind = "bigquery"
BigqueryToolKind = "bigquery-sql"
BigqueryProject = os.Getenv("BIGQUERY_PROJECT")
)
func getBigQueryVars(t *testing.T) map[string]any {
switch "" {
case BIGQUERY_PROJECT:
case BigqueryProject:
t.Fatal("'BIGQUERY_PROJECT' not set")
}
return map[string]any{
"kind": BIGQUERY_SOURCE_KIND,
"project": BIGQUERY_PROJECT,
"kind": BigquerySourceKind,
"project": BigqueryProject,
}
}
@@ -76,7 +77,7 @@ func TestBigQueryToolEndpoints(t *testing.T) {
var args []string
client, err := initBigQueryConnection(BIGQUERY_PROJECT)
client, err := initBigQueryConnection(BigqueryProject)
if err != nil {
t.Fatalf("unable to create Cloud SQL connection pool: %s", err)
}
@@ -85,36 +86,36 @@ func TestBigQueryToolEndpoints(t *testing.T) {
datasetName := fmt.Sprintf("temp_toolbox_test_%s", strings.ReplaceAll(uuid.New().String(), "-", ""))
tableName := fmt.Sprintf("param_table_%s", strings.ReplaceAll(uuid.New().String(), "-", ""))
tableNameParam := fmt.Sprintf("`%s.%s.%s`",
BIGQUERY_PROJECT,
BigqueryProject,
datasetName,
tableName,
)
tableNameAuth := fmt.Sprintf("`%s.%s.auth_table_%s`",
BIGQUERY_PROJECT,
BigqueryProject,
datasetName,
strings.ReplaceAll(uuid.New().String(), "-", ""),
)
tableNameTemplateParam := fmt.Sprintf("`%s.%s.template_param_table_%s`",
BIGQUERY_PROJECT,
BigqueryProject,
datasetName,
strings.ReplaceAll(uuid.New().String(), "-", ""),
)
// set up data for param tool
create_statement1, insert_statement1, tool_statement1, params1 := getBigQueryParamToolInfo(tableNameParam)
teardownTable1 := setupBigQueryTable(t, ctx, client, create_statement1, insert_statement1, datasetName, tableNameParam, params1)
createStatement1, insertStatement1, paramToolStatement1, paramToolStatement2, params1 := getBigQueryParamToolInfo(tableNameParam)
teardownTable1 := setupBigQueryTable(t, ctx, client, createStatement1, insertStatement1, datasetName, tableNameParam, params1)
defer teardownTable1(t)
// set up data for auth tool
create_statement2, insert_statement2, tool_statement2, params2 := getBigQueryAuthToolInfo(tableNameAuth)
teardownTable2 := setupBigQueryTable(t, ctx, client, create_statement2, insert_statement2, datasetName, tableNameAuth, params2)
createStatement2, insertStatement2, authToolStatement, params2 := getBigQueryAuthToolInfo(tableNameAuth)
teardownTable2 := setupBigQueryTable(t, ctx, client, createStatement2, insertStatement2, datasetName, tableNameAuth, params2)
defer teardownTable2(t)
// Write config into a file and pass it to command
toolsFile := tests.GetToolsConfig(sourceConfig, BIGQUERY_TOOL_KIND, tool_statement1, tool_statement2)
toolsFile := tests.GetToolsConfig(sourceConfig, BigqueryToolKind, paramToolStatement1, paramToolStatement2, authToolStatement)
toolsFile = addBigQueryPrebuiltToolsConfig(t, toolsFile)
tmplSelectCombined, tmplSelectFilterCombined := getBigQueryTmplToolStatement()
toolsFile = tests.AddTemplateParamConfig(t, toolsFile, BIGQUERY_TOOL_KIND, tmplSelectCombined, tmplSelectFilterCombined, "")
toolsFile = tests.AddTemplateParamConfig(t, toolsFile, BigqueryToolKind, tmplSelectCombined, tmplSelectFilterCombined, "")
cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...)
if err != nil {
@@ -124,7 +125,7 @@ func TestBigQueryToolEndpoints(t *testing.T) {
waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
out, err := cmd.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`))
out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out)
if err != nil {
t.Logf("toolbox command logs: \n%s", out)
t.Fatalf("toolbox didn't start successfully: %s", err)
@@ -137,8 +138,8 @@ func TestBigQueryToolEndpoints(t *testing.T) {
failInvocationWant := `{"jsonrpc":"2.0","id":"invoke-fail-tool","result":{"content":[{"type":"text","text":"unable to execute query: googleapi: Error 400: Syntax error: Unexpected identifier \"SELEC\" at [1:1]`
datasetInfoWant := "\"Location\":\"US\",\"DefaultTableExpiration\":0,\"Labels\":null,\"Access\":"
tableInfoWant := "[{\"Name\":\"\",\"Location\":\"US\",\"Description\":\"\",\"Schema\":[{\"Name\":\"id\""
invokeParamWant, mcpInvokeParamWant := tests.GetNonSpannerInvokeParamWant()
tests.RunToolInvokeTest(t, select1Want, invokeParamWant)
invokeParamWant, invokeParamWantNull, mcpInvokeParamWant := tests.GetNonSpannerInvokeParamWant()
tests.RunToolInvokeTest(t, select1Want, invokeParamWant, invokeParamWantNull)
tests.RunMCPToolCallMethod(t, mcpInvokeParamWant, failInvocationWant)
templateParamTestConfig := tests.NewTemplateParameterTestConfig(
tests.WithCreateColArray(`["id INT64", "name STRING", "age INT64"]`),
@@ -153,18 +154,20 @@ func TestBigQueryToolEndpoints(t *testing.T) {
}
// getBigQueryParamToolInfo returns statements and param for my-param-tool for bigquery kind
func getBigQueryParamToolInfo(tableName string) (string, string, string, []bigqueryapi.QueryParameter) {
func getBigQueryParamToolInfo(tableName string) (string, string, string, string, []bigqueryapi.QueryParameter) {
createStatement := fmt.Sprintf(`
CREATE TABLE IF NOT EXISTS %s (id INT64, name STRING);`, tableName)
insertStatement := fmt.Sprintf(`
INSERT INTO %s (id, name) VALUES (?, ?), (?, ?), (?, ?);`, tableName)
INSERT INTO %s (id, name) VALUES (?, ?), (?, ?), (?, ?), (?, NULL);`, tableName)
toolStatement := fmt.Sprintf(`SELECT * FROM %s WHERE id = ? OR name = ? ORDER BY id;`, tableName)
toolStatement2 := fmt.Sprintf(`SELECT * FROM %s WHERE id = ? ORDER BY id;`, tableName)
params := []bigqueryapi.QueryParameter{
{Value: int64(1)}, {Value: "Alice"},
{Value: int64(2)}, {Value: "Jane"},
{Value: int64(3)}, {Value: "Sid"},
{Value: int64(4)},
}
return createStatement, insertStatement, toolStatement, params
return createStatement, insertStatement, toolStatement, toolStatement2, params
}
// getBigQueryAuthToolInfo returns statements and param of my-auth-tool for bigquery kind
@@ -176,7 +179,7 @@ func getBigQueryAuthToolInfo(tableName string) (string, string, string, []bigque
toolStatement := fmt.Sprintf(`
SELECT name FROM %s WHERE email = ?`, tableName)
params := []bigqueryapi.QueryParameter{
{Value: int64(1)}, {Value: "Alice"}, {Value: tests.SERVICE_ACCOUNT_EMAIL},
{Value: int64(1)}, {Value: "Alice"}, {Value: tests.ServiceAccountEmail},
{Value: int64(2)}, {Value: "Jane"}, {Value: "janedoe@gmail.com"},
}
return createStatement, insertStatement, toolStatement, params
@@ -189,7 +192,7 @@ func getBigQueryTmplToolStatement() (string, string) {
return tmplSelectCombined, tmplSelectFilterCombined
}
func setupBigQueryTable(t *testing.T, ctx context.Context, client *bigqueryapi.Client, create_statement, insert_statement, datasetName string, tableName string, params []bigqueryapi.QueryParameter) func(*testing.T) {
func setupBigQueryTable(t *testing.T, ctx context.Context, client *bigqueryapi.Client, createStatement, insertStatement, datasetName string, tableName string, params []bigqueryapi.QueryParameter) func(*testing.T) {
// Create dataset
dataset := client.Dataset(datasetName)
_, err := dataset.Metadata(ctx)
@@ -206,7 +209,7 @@ func setupBigQueryTable(t *testing.T, ctx context.Context, client *bigqueryapi.C
}
// Create table
createJob, err := client.Query(create_statement).Run(ctx)
createJob, err := client.Query(createStatement).Run(ctx)
if err != nil {
t.Fatalf("Failed to start create table job for %s: %v", tableName, err)
@@ -220,7 +223,7 @@ func setupBigQueryTable(t *testing.T, ctx context.Context, client *bigqueryapi.C
}
// Insert test data
insertQuery := client.Query(insert_statement)
insertQuery := client.Query(insertStatement)
insertQuery.Parameters = params
insertJob, err := insertQuery.Run(ctx)
if err != nil {
@@ -340,7 +343,7 @@ func addBigQueryPrebuiltToolsConfig(t *testing.T, config map[string]any) map[str
return config
}
func runBigQueryExecuteSqlToolInvokeTest(t *testing.T, select_1_want, invokeParamWant, tableNameParam string) {
func runBigQueryExecuteSqlToolInvokeTest(t *testing.T, select1Want, invokeParamWant, tableNameParam string) {
// Get ID token
idToken, err := tests.GetGoogleIdToken(tests.ClientId)
if err != nil {
@@ -368,7 +371,7 @@ func runBigQueryExecuteSqlToolInvokeTest(t *testing.T, select_1_want, invokePara
api: "http://127.0.0.1:5000/api/tool/my-exec-sql-tool/invoke",
requestHeader: map[string]string{},
requestBody: bytes.NewBuffer([]byte(`{"sql":"SELECT 1"}`)),
want: select_1_want,
want: select1Want,
isErr: false,
},
{
@@ -414,7 +417,7 @@ func runBigQueryExecuteSqlToolInvokeTest(t *testing.T, select_1_want, invokePara
requestHeader: map[string]string{"my-google-auth_token": idToken},
requestBody: bytes.NewBuffer([]byte(`{"sql":"SELECT 1"}`)),
isErr: false,
want: select_1_want,
want: select1Want,
},
{
name: "Invoke my-auth-exec-sql-tool with invalid auth token",
@@ -499,6 +502,21 @@ func runBigQueryListDatasetToolInvokeTest(t *testing.T, datasetWant string) {
isErr: false,
want: datasetWant,
},
{
name: "invoke my-list-dataset-ids-tool with project",
api: "http://127.0.0.1:5000/api/tool/my-auth-list-dataset-ids-tool/invoke",
requestHeader: map[string]string{"my-google-auth_token": idToken},
requestBody: bytes.NewBuffer([]byte(fmt.Sprintf("{\"project\":\"%s\"}", BigqueryProject))),
isErr: false,
want: datasetWant,
},
{
name: "invoke my-list-dataset-ids-tool with non-existent project",
api: "http://127.0.0.1:5000/api/tool/my-auth-list-dataset-ids-tool/invoke",
requestHeader: map[string]string{"my-google-auth_token": idToken},
requestBody: bytes.NewBuffer([]byte(fmt.Sprintf("{\"project\":\"%s-%s\"}", BigqueryProject, uuid.NewString()))),
isErr: true,
},
{
name: "invoke my-auth-list-dataset-ids-tool",
api: "http://127.0.0.1:5000/api/tool/my-auth-list-dataset-ids-tool/invoke",
@@ -583,6 +601,21 @@ func runBigQueryGetDatasetInfoToolInvokeTest(t *testing.T, datasetName, datasetI
want: datasetInfoWant,
isErr: false,
},
{
name: "Invoke my-auth-get-dataset-info-tool with correct project",
api: "http://127.0.0.1:5000/api/tool/my-auth-get-dataset-info-tool/invoke",
requestHeader: map[string]string{"my-google-auth_token": idToken},
requestBody: bytes.NewBuffer([]byte(fmt.Sprintf("{\"project\":\"%s\", \"dataset\":\"%s\"}", BigqueryProject, datasetName))),
want: datasetInfoWant,
isErr: false,
},
{
name: "Invoke my-auth-get-dataset-info-tool with non-existent project",
api: "http://127.0.0.1:5000/api/tool/my-auth-get-dataset-info-tool/invoke",
requestHeader: map[string]string{"my-google-auth_token": idToken},
requestBody: bytes.NewBuffer([]byte(fmt.Sprintf("{\"project\":\"%s-%s\", \"dataset\":\"%s\"}", BigqueryProject, uuid.NewString(), datasetName))),
isErr: true,
},
{
name: "invoke my-auth-get-dataset-info-tool without body",
api: "http://127.0.0.1:5000/api/tool/my-get-dataset-info-tool/invoke",
@@ -703,6 +736,21 @@ func runBigQueryListTableIdsToolInvokeTest(t *testing.T, datasetName, tablename_
want: tablename_want,
isErr: false,
},
{
name: "Invoke my-auth-list-table-ids-tool with correct project",
api: "http://127.0.0.1:5000/api/tool/my-auth-list-table-ids-tool/invoke",
requestHeader: map[string]string{"my-google-auth_token": idToken},
requestBody: bytes.NewBuffer([]byte(fmt.Sprintf("{\"project\":\"%s\", \"dataset\":\"%s\"}", BigqueryProject, datasetName))),
want: tablename_want,
isErr: false,
},
{
name: "Invoke my-auth-list-table-ids-tool with non-existent project",
api: "http://127.0.0.1:5000/api/tool/my-auth-list-table-ids-tool/invoke",
requestHeader: map[string]string{"my-google-auth_token": idToken},
requestBody: bytes.NewBuffer([]byte(fmt.Sprintf("{\"project\":\"%s-%s\", \"dataset\":\"%s\"}", BigqueryProject, uuid.NewString(), datasetName))),
isErr: true,
},
{
name: "Invoke my-auth-list-table-ids-tool with invalid auth token",
api: "http://127.0.0.1:5000/api/tool/my-auth-list-table-ids-tool/invoke",
@@ -808,6 +856,21 @@ func runBigQueryGetTableInfoToolInvokeTest(t *testing.T, datasetName, tableName,
want: tableInfoWant,
isErr: false,
},
{
name: "Invoke my-auth-get-table-info-tool with correct project",
api: "http://127.0.0.1:5000/api/tool/my-auth-get-table-info-tool/invoke",
requestHeader: map[string]string{"my-google-auth_token": idToken},
requestBody: bytes.NewBuffer([]byte(fmt.Sprintf("{\"project\":\"%s\", \"dataset\":\"%s\", \"table\":\"%s\"}", BigqueryProject, datasetName, tableName))),
want: tableInfoWant,
isErr: false,
},
{
name: "Invoke my-auth-get-table-info-tool with non-existent project",
api: "http://127.0.0.1:5000/api/tool/my-auth-get-table-info-tool/invoke",
requestHeader: map[string]string{"my-google-auth_token": idToken},
requestBody: bytes.NewBuffer([]byte(fmt.Sprintf("{\"project\":\"%s-%s\", \"dataset\":\"%s\", \"table\":\"%s\"}", BigqueryProject, uuid.NewString(), datasetName, tableName))),
isErr: true,
},
{
name: "Invoke my-auth-get-table-info-tool with invalid auth token",
api: "http://127.0.0.1:5000/api/tool/my-auth-get-table-info-tool/invoke",

View File

@@ -29,29 +29,30 @@ import (
"cloud.google.com/go/bigtable"
"github.com/google/uuid"
"github.com/googleapis/genai-toolbox/internal/testutils"
"github.com/googleapis/genai-toolbox/internal/tools"
"github.com/googleapis/genai-toolbox/tests"
)
var (
BIGTABLE_SOURCE_KIND = "bigtable"
BIGTABLE_TOOL_KIND = "bigtable-sql"
BIGTABLE_PROJECT = os.Getenv("BIGTABLE_PROJECT")
BIGTABLE_INSTANCE = os.Getenv("BIGTABLE_INSTANCE")
BigtableSourceKind = "bigtable"
BigtableToolKind = "bigtable-sql"
BigtableProject = os.Getenv("BIGTABLE_PROJECT")
BigtableInstance = os.Getenv("BIGTABLE_INSTANCE")
)
func getBigtableVars(t *testing.T) map[string]any {
switch "" {
case BIGTABLE_PROJECT:
case BigtableProject:
t.Fatal("'BIGTABLE_PROJECT' not set")
case BIGTABLE_INSTANCE:
case BigtableInstance:
t.Fatal("'BIGTABLE_INSTANCE' not set")
}
return map[string]any{
"kind": BIGTABLE_SOURCE_KIND,
"project": BIGTABLE_PROJECT,
"instance": BIGTABLE_INSTANCE,
"kind": BigtableSourceKind,
"project": BigtableProject,
"instance": BigtableInstance,
}
}
@@ -77,13 +78,14 @@ func TestBigtableToolEndpoints(t *testing.T) {
// Do not change the shape of statement without checking tests/common_test.go.
// The structure and value of seed data has to match https://github.com/googleapis/genai-toolbox/blob/4dba0df12dc438eca3cb476ef52aa17cdf232c12/tests/common_test.go#L200-L251
param_test_statement := fmt.Sprintf("SELECT TO_INT64(cf['id']) as id, CAST(cf['name'] AS string) as name, FROM %s WHERE TO_INT64(cf['id']) = @id OR CAST(cf['name'] AS string) = @name;", tableName)
paramTestStatement := fmt.Sprintf("SELECT TO_INT64(cf['id']) as id, CAST(cf['name'] AS string) as name, FROM %s WHERE TO_INT64(cf['id']) = @id OR CAST(cf['name'] AS string) = @name;", tableName)
paramTestStatement2 := fmt.Sprintf("SELECT TO_INT64(cf['id']) as id, CAST(cf['name'] AS string) as name, FROM %s WHERE TO_INT64(cf['id']) = @id;", tableName)
teardownTable1 := setupBtTable(t, ctx, sourceConfig["project"].(string), sourceConfig["instance"].(string), tableName, columnFamilyName, muts, rowKeys)
defer teardownTable1(t)
// Do not change the shape of statement without checking tests/common_test.go.
// The structure and value of seed data has to match https://github.com/googleapis/genai-toolbox/blob/4dba0df12dc438eca3cb476ef52aa17cdf232c12/tests/common_test.go#L200-L251
auth_tool_statement := fmt.Sprintf("SELECT CAST(cf['name'] AS string) as name FROM %s WHERE CAST(cf['email'] AS string) = @email;", tableNameAuth)
authToolStatement := fmt.Sprintf("SELECT CAST(cf['name'] AS string) as name FROM %s WHERE CAST(cf['email'] AS string) = @email;", tableNameAuth)
teardownTable2 := setupBtTable(t, ctx, sourceConfig["project"].(string), sourceConfig["instance"].(string), tableNameAuth, columnFamilyName, muts, rowKeys)
defer teardownTable2(t)
@@ -92,7 +94,7 @@ func TestBigtableToolEndpoints(t *testing.T) {
defer teardownTableTmpl(t)
// Write config into a file and pass it to command
toolsFile := tests.GetToolsConfig(sourceConfig, BIGTABLE_TOOL_KIND, param_test_statement, auth_tool_statement)
toolsFile := tests.GetToolsConfig(sourceConfig, BigtableToolKind, paramTestStatement, paramTestStatement2, authToolStatement)
toolsFile = addTemplateParamConfig(t, toolsFile)
cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...)
@@ -103,7 +105,7 @@ func TestBigtableToolEndpoints(t *testing.T) {
waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
out, err := cmd.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`))
out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out)
if err != nil {
t.Logf("toolbox command logs: \n%s", out)
t.Fatalf("toolbox didn't start successfully: %s", err)
@@ -114,8 +116,9 @@ func TestBigtableToolEndpoints(t *testing.T) {
// Actual test parameters are set in https://github.com/googleapis/genai-toolbox/blob/52b09a67cb40ac0c5f461598b4673136699a3089/tests/tool_test.go#L250
select1Want := "[{\"$col1\":1}]"
failInvocationWant := `{"jsonrpc":"2.0","id":"invoke-fail-tool","result":{"content":[{"type":"text","text":"unable to prepare statement: rpc error: code = InvalidArgument desc = Syntax error: Unexpected identifier \"SELEC\" [at 1:1]"}],"isError":true}}`
invokeParamWant, mcpInvokeParamWant := tests.GetNonSpannerInvokeParamWant()
tests.RunToolInvokeTest(t, select1Want, invokeParamWant)
invokeParamWant, _, mcpInvokeParamWant := tests.GetNonSpannerInvokeParamWant()
invokeParamWantNull := `[{"id":4,"name":""}]`
tests.RunToolInvokeTest(t, select1Want, invokeParamWant, invokeParamWantNull)
tests.RunMCPToolCallMethod(t, mcpInvokeParamWant, failInvocationWant)
templateParamTestConfig := tests.NewTemplateParameterTestConfig(
@@ -139,7 +142,7 @@ func getTestData(columnFamilyName string) ([]*bigtable.Mutation, []string) {
muts := []*bigtable.Mutation{}
rowKeys := []string{}
var ids [3][]byte
var ids [4][]byte
for i := range ids {
ids[i] = convertToBytes(i + 1)
}
@@ -151,7 +154,7 @@ func getTestData(columnFamilyName string) ([]*bigtable.Mutation, []string) {
// Expected values are defined in https://github.com/googleapis/genai-toolbox/blob/52b09a67cb40ac0c5f461598b4673136699a3089/tests/tool_test.go#L229-L310
"row-01": {
"name": []byte("Alice"),
"email": []byte(tests.SERVICE_ACCOUNT_EMAIL),
"email": []byte(tests.ServiceAccountEmail),
"id": ids[0],
},
"row-02": {
@@ -163,6 +166,10 @@ func getTestData(columnFamilyName string) ([]*bigtable.Mutation, []string) {
"name": []byte("Sid"),
"id": ids[2],
},
"row-04": {
"name": nil,
"id": ids[3],
},
} {
mut := bigtable.NewMutation()
for col, v := range mutData {

View File

@@ -29,53 +29,54 @@ import (
"cloud.google.com/go/cloudsqlconn"
"cloud.google.com/go/cloudsqlconn/sqlserver/mssql"
"github.com/google/uuid"
"github.com/googleapis/genai-toolbox/internal/testutils"
"github.com/googleapis/genai-toolbox/tests"
)
var (
CLOUD_SQL_MSSQL_SOURCE_KIND = "cloud-sql-mssql"
CLOUD_SQL_MSSQL_TOOL_KIND = "mssql-sql"
CLOUD_SQL_MSSQL_PROJECT = os.Getenv("CLOUD_SQL_MSSQL_PROJECT")
CLOUD_SQL_MSSQL_REGION = os.Getenv("CLOUD_SQL_MSSQL_REGION")
CLOUD_SQL_MSSQL_INSTANCE = os.Getenv("CLOUD_SQL_MSSQL_INSTANCE")
CLOUD_SQL_MSSQL_DATABASE = os.Getenv("CLOUD_SQL_MSSQL_DATABASE")
CLOUD_SQL_MSSQL_IP = os.Getenv("CLOUD_SQL_MSSQL_IP")
CLOUD_SQL_MSSQL_USER = os.Getenv("CLOUD_SQL_MSSQL_USER")
CLOUD_SQL_MSSQL_PASS = os.Getenv("CLOUD_SQL_MSSQL_PASS")
CloudSQLMSSQLSourceKind = "cloud-sql-mssql"
CloudSQLMSSQLToolKind = "mssql-sql"
CloudSQLMSSQLProject = os.Getenv("CLOUD_SQL_MSSQL_PROJECT")
CloudSQLMSSQLRegion = os.Getenv("CLOUD_SQL_MSSQL_REGION")
CloudSQLMSSQLInstance = os.Getenv("CLOUD_SQL_MSSQL_INSTANCE")
CloudSQLMSSQLDatabase = os.Getenv("CLOUD_SQL_MSSQL_DATABASE")
CloudSQLMSSQLIp = os.Getenv("CLOUD_SQL_MSSQL_IP")
CloudSQLMSSQLUser = os.Getenv("CLOUD_SQL_MSSQL_USER")
CloudSQLMSSQLPass = os.Getenv("CLOUD_SQL_MSSQL_PASS")
)
func getCloudSQLMssqlVars(t *testing.T) map[string]any {
func getCloudSQLMSSQLVars(t *testing.T) map[string]any {
switch "" {
case CLOUD_SQL_MSSQL_PROJECT:
case CloudSQLMSSQLProject:
t.Fatal("'CLOUD_SQL_MSSQL_PROJECT' not set")
case CLOUD_SQL_MSSQL_REGION:
case CloudSQLMSSQLRegion:
t.Fatal("'CLOUD_SQL_MSSQL_REGION' not set")
case CLOUD_SQL_MSSQL_INSTANCE:
case CloudSQLMSSQLInstance:
t.Fatal("'CLOUD_SQL_MSSQL_INSTANCE' not set")
case CLOUD_SQL_MSSQL_IP:
case CloudSQLMSSQLIp:
t.Fatal("'CLOUD_SQL_MSSQL_IP' not set")
case CLOUD_SQL_MSSQL_DATABASE:
case CloudSQLMSSQLDatabase:
t.Fatal("'CLOUD_SQL_MSSQL_DATABASE' not set")
case CLOUD_SQL_MSSQL_USER:
case CloudSQLMSSQLUser:
t.Fatal("'CLOUD_SQL_MSSQL_USER' not set")
case CLOUD_SQL_MSSQL_PASS:
case CloudSQLMSSQLPass:
t.Fatal("'CLOUD_SQL_MSSQL_PASS' not set")
}
return map[string]any{
"kind": CLOUD_SQL_MSSQL_SOURCE_KIND,
"project": CLOUD_SQL_MSSQL_PROJECT,
"instance": CLOUD_SQL_MSSQL_INSTANCE,
"ipAddress": CLOUD_SQL_MSSQL_IP,
"region": CLOUD_SQL_MSSQL_REGION,
"database": CLOUD_SQL_MSSQL_DATABASE,
"user": CLOUD_SQL_MSSQL_USER,
"password": CLOUD_SQL_MSSQL_PASS,
"kind": CloudSQLMSSQLSourceKind,
"project": CloudSQLMSSQLProject,
"instance": CloudSQLMSSQLInstance,
"ipAddress": CloudSQLMSSQLIp,
"region": CloudSQLMSSQLRegion,
"database": CloudSQLMSSQLDatabase,
"user": CloudSQLMSSQLUser,
"password": CloudSQLMSSQLPass,
}
}
// Copied over from cloud_sql_mssql.go
func initCloudSQLMssqlConnection(project, region, instance, ipAddress, ipType, user, pass, dbname string) (*sql.DB, error) {
func initCloudSQLMSSQLConnection(project, region, instance, ipAddress, ipType, user, pass, dbname string) (*sql.DB, error) {
// Create dsn
query := fmt.Sprintf("database=%s&cloudsql=%s:%s:%s", dbname, project, region, instance)
url := &url.URL{
@@ -110,14 +111,14 @@ func initCloudSQLMssqlConnection(project, region, instance, ipAddress, ipType, u
return db, nil
}
func TestCloudSQLMssqlToolEndpoints(t *testing.T) {
sourceConfig := getCloudSQLMssqlVars(t)
func TestCloudSQLMSSQLToolEndpoints(t *testing.T) {
sourceConfig := getCloudSQLMSSQLVars(t)
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
var args []string
db, err := initCloudSQLMssqlConnection(CLOUD_SQL_MSSQL_PROJECT, CLOUD_SQL_MSSQL_REGION, CLOUD_SQL_MSSQL_INSTANCE, CLOUD_SQL_MSSQL_IP, "public", CLOUD_SQL_MSSQL_USER, CLOUD_SQL_MSSQL_PASS, CLOUD_SQL_MSSQL_DATABASE)
db, err := initCloudSQLMSSQLConnection(CloudSQLMSSQLProject, CloudSQLMSSQLRegion, CloudSQLMSSQLInstance, CloudSQLMSSQLIp, "public", CloudSQLMSSQLUser, CloudSQLMSSQLPass, CloudSQLMSSQLDatabase)
if err != nil {
t.Fatalf("unable to create Cloud SQL connection pool: %s", err)
}
@@ -128,20 +129,20 @@ func TestCloudSQLMssqlToolEndpoints(t *testing.T) {
tableNameTemplateParam := "template_param_table_" + strings.ReplaceAll(uuid.New().String(), "-", "")
// set up data for param tool
create_statement1, insert_statement1, tool_statement1, params1 := tests.GetMssqlParamToolInfo(tableNameParam)
teardownTable1 := tests.SetupMsSQLTable(t, ctx, db, create_statement1, insert_statement1, tableNameParam, params1)
createStatement1, insertStatement1, paramToolStatement1, paramToolStatement2, params1 := tests.GetMSSQLParamToolInfo(tableNameParam)
teardownTable1 := tests.SetupMsSQLTable(t, ctx, db, createStatement1, insertStatement1, tableNameParam, params1)
defer teardownTable1(t)
// set up data for auth tool
create_statement2, insert_statement2, tool_statement2, params2 := tests.GetMssqlAuthToolInfo(tableNameAuth)
teardownTable2 := tests.SetupMsSQLTable(t, ctx, db, create_statement2, insert_statement2, tableNameAuth, params2)
createStatement2, insertStatement2, authToolStatement, params2 := tests.GetMSSQLAuthToolInfo(tableNameAuth)
teardownTable2 := tests.SetupMsSQLTable(t, ctx, db, createStatement2, insertStatement2, tableNameAuth, params2)
defer teardownTable2(t)
// Write config into a file and pass it to command
toolsFile := tests.GetToolsConfig(sourceConfig, CLOUD_SQL_MSSQL_TOOL_KIND, tool_statement1, tool_statement2)
toolsFile = tests.AddMssqlExecuteSqlConfig(t, toolsFile)
tmplSelectCombined, tmplSelectFilterCombined := tests.GetMssqlTmplToolStatement()
toolsFile = tests.AddTemplateParamConfig(t, toolsFile, CLOUD_SQL_MSSQL_TOOL_KIND, tmplSelectCombined, tmplSelectFilterCombined, "")
toolsFile := tests.GetToolsConfig(sourceConfig, CloudSQLMSSQLToolKind, paramToolStatement1, paramToolStatement2, authToolStatement)
toolsFile = tests.AddMSSQLExecuteSqlConfig(t, toolsFile)
tmplSelectCombined, tmplSelectFilterCombined := tests.GetMSSQLTmplToolStatement()
toolsFile = tests.AddTemplateParamConfig(t, toolsFile, CloudSQLMSSQLToolKind, tmplSelectCombined, tmplSelectFilterCombined, "")
cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...)
if err != nil {
@@ -151,7 +152,7 @@ func TestCloudSQLMssqlToolEndpoints(t *testing.T) {
waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
out, err := cmd.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`))
out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out)
if err != nil {
t.Logf("toolbox command logs: \n%s", out)
t.Fatalf("toolbox didn't start successfully: %s", err)
@@ -159,17 +160,17 @@ func TestCloudSQLMssqlToolEndpoints(t *testing.T) {
tests.RunToolGetTest(t)
select1Want, failInvocationWant, createTableStatement := tests.GetMssqlWants()
invokeParamWant, mcpInvokeParamWant := tests.GetNonSpannerInvokeParamWant()
tests.RunToolInvokeTest(t, select1Want, invokeParamWant)
select1Want, failInvocationWant, createTableStatement := tests.GetMSSQLWants()
invokeParamWant, invokeParamWantNull, mcpInvokeParamWant := tests.GetNonSpannerInvokeParamWant()
tests.RunToolInvokeTest(t, select1Want, invokeParamWant, invokeParamWantNull)
tests.RunExecuteSqlToolInvokeTest(t, createTableStatement, select1Want)
tests.RunMCPToolCallMethod(t, mcpInvokeParamWant, failInvocationWant)
tests.RunToolInvokeWithTemplateParameters(t, tableNameTemplateParam, tests.NewTemplateParameterTestConfig())
}
// Test connection with different IP type
func TestCloudSQLMssqlIpConnection(t *testing.T) {
sourceConfig := getCloudSQLMssqlVars(t)
func TestCloudSQLMSSQLIpConnection(t *testing.T) {
sourceConfig := getCloudSQLMSSQLVars(t)
tcs := []struct {
name string
@@ -187,7 +188,7 @@ func TestCloudSQLMssqlIpConnection(t *testing.T) {
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
sourceConfig["ipType"] = tc.ipType
err := tests.RunSourceConnectionTest(t, sourceConfig, CLOUD_SQL_MSSQL_TOOL_KIND)
err := tests.RunSourceConnectionTest(t, sourceConfig, CloudSQLMSSQLToolKind)
if err != nil {
t.Fatalf("Connection test failure: %s", err)
}

View File

@@ -28,44 +28,45 @@ import (
"cloud.google.com/go/cloudsqlconn"
"cloud.google.com/go/cloudsqlconn/mysql/mysql"
"github.com/google/uuid"
"github.com/googleapis/genai-toolbox/internal/testutils"
"github.com/googleapis/genai-toolbox/tests"
)
var (
CLOUD_SQL_MYSQL_SOURCE_KIND = "cloud-sql-mysql"
CLOUD_SQL_MYSQL_TOOL_KIND = "mysql-sql"
CLOUD_SQL_MYSQL_PROJECT = os.Getenv("CLOUD_SQL_MYSQL_PROJECT")
CLOUD_SQL_MYSQL_REGION = os.Getenv("CLOUD_SQL_MYSQL_REGION")
CLOUD_SQL_MYSQL_INSTANCE = os.Getenv("CLOUD_SQL_MYSQL_INSTANCE")
CLOUD_SQL_MYSQL_DATABASE = os.Getenv("CLOUD_SQL_MYSQL_DATABASE")
CLOUD_SQL_MYSQL_USER = os.Getenv("CLOUD_SQL_MYSQL_USER")
CLOUD_SQL_MYSQL_PASS = os.Getenv("CLOUD_SQL_MYSQL_PASS")
CloudSQLMySQLSourceKind = "cloud-sql-mysql"
CloudSQLMySQLToolKind = "mysql-sql"
CloudSQLMySQLProject = os.Getenv("CLOUD_SQL_MYSQL_PROJECT")
CloudSQLMySQLRegion = os.Getenv("CLOUD_SQL_MYSQL_REGION")
CloudSQLMySQLInstance = os.Getenv("CLOUD_SQL_MYSQL_INSTANCE")
CloudSQLMySQLDatabase = os.Getenv("CLOUD_SQL_MYSQL_DATABASE")
CloudSQLMySQLUser = os.Getenv("CLOUD_SQL_MYSQL_USER")
CloudSQLMySQLPass = os.Getenv("CLOUD_SQL_MYSQL_PASS")
)
func getCloudSQLMySQLVars(t *testing.T) map[string]any {
switch "" {
case CLOUD_SQL_MYSQL_PROJECT:
case CloudSQLMySQLProject:
t.Fatal("'CLOUD_SQL_MYSQL_PROJECT' not set")
case CLOUD_SQL_MYSQL_REGION:
case CloudSQLMySQLRegion:
t.Fatal("'CLOUD_SQL_MYSQL_REGION' not set")
case CLOUD_SQL_MYSQL_INSTANCE:
case CloudSQLMySQLInstance:
t.Fatal("'CLOUD_SQL_MYSQL_INSTANCE' not set")
case CLOUD_SQL_MYSQL_DATABASE:
case CloudSQLMySQLDatabase:
t.Fatal("'CLOUD_SQL_MYSQL_DATABASE' not set")
case CLOUD_SQL_MYSQL_USER:
case CloudSQLMySQLUser:
t.Fatal("'CLOUD_SQL_MYSQL_USER' not set")
case CLOUD_SQL_MYSQL_PASS:
case CloudSQLMySQLPass:
t.Fatal("'CLOUD_SQL_MYSQL_PASS' not set")
}
return map[string]any{
"kind": CLOUD_SQL_MYSQL_SOURCE_KIND,
"project": CLOUD_SQL_MYSQL_PROJECT,
"instance": CLOUD_SQL_MYSQL_INSTANCE,
"region": CLOUD_SQL_MYSQL_REGION,
"database": CLOUD_SQL_MYSQL_DATABASE,
"user": CLOUD_SQL_MYSQL_USER,
"password": CLOUD_SQL_MYSQL_PASS,
"kind": CloudSQLMySQLSourceKind,
"project": CloudSQLMySQLProject,
"instance": CloudSQLMySQLInstance,
"region": CloudSQLMySQLRegion,
"database": CloudSQLMySQLDatabase,
"user": CloudSQLMySQLUser,
"password": CloudSQLMySQLPass,
}
}
@@ -97,14 +98,14 @@ func initCloudSQLMySQLConnectionPool(project, region, instance, ipType, user, pa
return db, nil
}
func TestCloudSQLMysqlToolEndpoints(t *testing.T) {
func TestCloudSQLMySQLToolEndpoints(t *testing.T) {
sourceConfig := getCloudSQLMySQLVars(t)
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
var args []string
pool, err := initCloudSQLMySQLConnectionPool(CLOUD_SQL_MYSQL_PROJECT, CLOUD_SQL_MYSQL_REGION, CLOUD_SQL_MYSQL_INSTANCE, "public", CLOUD_SQL_MYSQL_USER, CLOUD_SQL_MYSQL_PASS, CLOUD_SQL_MYSQL_DATABASE)
pool, err := initCloudSQLMySQLConnectionPool(CloudSQLMySQLProject, CloudSQLMySQLRegion, CloudSQLMySQLInstance, "public", CloudSQLMySQLUser, CloudSQLMySQLPass, CloudSQLMySQLDatabase)
if err != nil {
t.Fatalf("unable to create Cloud SQL connection pool: %s", err)
}
@@ -115,20 +116,20 @@ func TestCloudSQLMysqlToolEndpoints(t *testing.T) {
tableNameTemplateParam := "template_param_table_" + strings.ReplaceAll(uuid.New().String(), "-", "")
// set up data for param tool
create_statement1, insert_statement1, tool_statement1, params1 := tests.GetMysqlParamToolInfo(tableNameParam)
teardownTable1 := tests.SetupMySQLTable(t, ctx, pool, create_statement1, insert_statement1, tableNameParam, params1)
createStatement1, insertStatement1, paramToolStatement1, paramToolStatement2, params1 := tests.GetMySQLParamToolInfo(tableNameParam)
teardownTable1 := tests.SetupMySQLTable(t, ctx, pool, createStatement1, insertStatement1, tableNameParam, params1)
defer teardownTable1(t)
// set up data for auth tool
create_statement2, insert_statement2, tool_statement2, params2 := tests.GetMysqlAuthToolInfo(tableNameAuth)
teardownTable2 := tests.SetupMySQLTable(t, ctx, pool, create_statement2, insert_statement2, tableNameAuth, params2)
createStatement2, insertStatement2, authToolStatement, params2 := tests.GetMySQLAuthToolInfo(tableNameAuth)
teardownTable2 := tests.SetupMySQLTable(t, ctx, pool, createStatement2, insertStatement2, tableNameAuth, params2)
defer teardownTable2(t)
// Write config into a file and pass it to command
toolsFile := tests.GetToolsConfig(sourceConfig, CLOUD_SQL_MYSQL_TOOL_KIND, tool_statement1, tool_statement2)
toolsFile := tests.GetToolsConfig(sourceConfig, CloudSQLMySQLToolKind, paramToolStatement1, paramToolStatement2, authToolStatement)
toolsFile = tests.AddMySqlExecuteSqlConfig(t, toolsFile)
tmplSelectCombined, tmplSelectFilterCombined := tests.GetMysqlTmplToolStatement()
toolsFile = tests.AddTemplateParamConfig(t, toolsFile, CLOUD_SQL_MYSQL_TOOL_KIND, tmplSelectCombined, tmplSelectFilterCombined, "")
tmplSelectCombined, tmplSelectFilterCombined := tests.GetMySQLTmplToolStatement()
toolsFile = tests.AddTemplateParamConfig(t, toolsFile, CloudSQLMySQLToolKind, tmplSelectCombined, tmplSelectFilterCombined, "")
cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...)
if err != nil {
@@ -138,7 +139,7 @@ func TestCloudSQLMysqlToolEndpoints(t *testing.T) {
waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
out, err := cmd.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`))
out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out)
if err != nil {
t.Logf("toolbox command logs: \n%s", out)
t.Fatalf("toolbox didn't start successfully: %s", err)
@@ -146,16 +147,16 @@ func TestCloudSQLMysqlToolEndpoints(t *testing.T) {
tests.RunToolGetTest(t)
select1Want, failInvocationWant, createTableStatement := tests.GetMysqlWants()
invokeParamWant, mcpInvokeParamWant := tests.GetNonSpannerInvokeParamWant()
tests.RunToolInvokeTest(t, select1Want, invokeParamWant)
select1Want, failInvocationWant, createTableStatement := tests.GetMySQLWants()
invokeParamWant, invokeParamWantNull, mcpInvokeParamWant := tests.GetNonSpannerInvokeParamWant()
tests.RunToolInvokeTest(t, select1Want, invokeParamWant, invokeParamWantNull)
tests.RunExecuteSqlToolInvokeTest(t, createTableStatement, select1Want)
tests.RunMCPToolCallMethod(t, mcpInvokeParamWant, failInvocationWant)
tests.RunToolInvokeWithTemplateParameters(t, tableNameTemplateParam, tests.NewTemplateParameterTestConfig())
}
// Test connection with different IP type
func TestCloudSQLMysqlIpConnection(t *testing.T) {
func TestCloudSQLMySQLIpConnection(t *testing.T) {
sourceConfig := getCloudSQLMySQLVars(t)
tcs := []struct {
@@ -174,7 +175,7 @@ func TestCloudSQLMysqlIpConnection(t *testing.T) {
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
sourceConfig["ipType"] = tc.ipType
err := tests.RunSourceConnectionTest(t, sourceConfig, CLOUD_SQL_MYSQL_TOOL_KIND)
err := tests.RunSourceConnectionTest(t, sourceConfig, CloudSQLMySQLToolKind)
if err != nil {
t.Fatalf("Connection test failure: %s", err)
}

View File

@@ -26,45 +26,46 @@ import (
"cloud.google.com/go/cloudsqlconn"
"github.com/google/uuid"
"github.com/googleapis/genai-toolbox/internal/testutils"
"github.com/googleapis/genai-toolbox/tests"
"github.com/jackc/pgx/v5/pgxpool"
)
var (
CLOUD_SQL_POSTGRES_SOURCE_KIND = "cloud-sql-postgres"
CLOUD_SQL_POSTGRES_TOOL_KIND = "postgres-sql"
CLOUD_SQL_POSTGRES_PROJECT = os.Getenv("CLOUD_SQL_POSTGRES_PROJECT")
CLOUD_SQL_POSTGRES_REGION = os.Getenv("CLOUD_SQL_POSTGRES_REGION")
CLOUD_SQL_POSTGRES_INSTANCE = os.Getenv("CLOUD_SQL_POSTGRES_INSTANCE")
CLOUD_SQL_POSTGRES_DATABASE = os.Getenv("CLOUD_SQL_POSTGRES_DATABASE")
CLOUD_SQL_POSTGRES_USER = os.Getenv("CLOUD_SQL_POSTGRES_USER")
CLOUD_SQL_POSTGRES_PASS = os.Getenv("CLOUD_SQL_POSTGRES_PASS")
CloudSQLPostgresSourceKind = "cloud-sql-postgres"
CloudSQLPostgresToolKind = "postgres-sql"
CloudSQLPostgresProject = os.Getenv("CLOUD_SQL_POSTGRES_PROJECT")
CloudSQLPostgresRegion = os.Getenv("CLOUD_SQL_POSTGRES_REGION")
CloudSQLPostgresInstance = os.Getenv("CLOUD_SQL_POSTGRES_INSTANCE")
CloudSQLPostgresDatabase = os.Getenv("CLOUD_SQL_POSTGRES_DATABASE")
CloudSQLPostgresUser = os.Getenv("CLOUD_SQL_POSTGRES_USER")
CloudSQLPostgresPass = os.Getenv("CLOUD_SQL_POSTGRES_PASS")
)
func getCloudSQLPgVars(t *testing.T) map[string]any {
switch "" {
case CLOUD_SQL_POSTGRES_PROJECT:
case CloudSQLPostgresProject:
t.Fatal("'CLOUD_SQL_POSTGRES_PROJECT' not set")
case CLOUD_SQL_POSTGRES_REGION:
case CloudSQLPostgresRegion:
t.Fatal("'CLOUD_SQL_POSTGRES_REGION' not set")
case CLOUD_SQL_POSTGRES_INSTANCE:
case CloudSQLPostgresInstance:
t.Fatal("'CLOUD_SQL_POSTGRES_INSTANCE' not set")
case CLOUD_SQL_POSTGRES_DATABASE:
case CloudSQLPostgresDatabase:
t.Fatal("'CLOUD_SQL_POSTGRES_DATABASE' not set")
case CLOUD_SQL_POSTGRES_USER:
case CloudSQLPostgresUser:
t.Fatal("'CLOUD_SQL_POSTGRES_USER' not set")
case CLOUD_SQL_POSTGRES_PASS:
case CloudSQLPostgresPass:
t.Fatal("'CLOUD_SQL_POSTGRES_PASS' not set")
}
return map[string]any{
"kind": CLOUD_SQL_POSTGRES_SOURCE_KIND,
"project": CLOUD_SQL_POSTGRES_PROJECT,
"instance": CLOUD_SQL_POSTGRES_INSTANCE,
"region": CLOUD_SQL_POSTGRES_REGION,
"database": CLOUD_SQL_POSTGRES_DATABASE,
"user": CLOUD_SQL_POSTGRES_USER,
"password": CLOUD_SQL_POSTGRES_PASS,
"kind": CloudSQLPostgresSourceKind,
"project": CloudSQLPostgresProject,
"instance": CloudSQLPostgresInstance,
"region": CloudSQLPostgresRegion,
"database": CloudSQLPostgresDatabase,
"user": CloudSQLPostgresUser,
"password": CloudSQLPostgresPass,
}
}
@@ -108,7 +109,7 @@ func TestCloudSQLPgSimpleToolEndpoints(t *testing.T) {
var args []string
pool, err := initCloudSQLPgConnectionPool(CLOUD_SQL_POSTGRES_PROJECT, CLOUD_SQL_POSTGRES_REGION, CLOUD_SQL_POSTGRES_INSTANCE, "public", CLOUD_SQL_POSTGRES_USER, CLOUD_SQL_POSTGRES_PASS, CLOUD_SQL_POSTGRES_DATABASE)
pool, err := initCloudSQLPgConnectionPool(CloudSQLPostgresProject, CloudSQLPostgresRegion, CloudSQLPostgresInstance, "public", CloudSQLPostgresUser, CloudSQLPostgresPass, CloudSQLPostgresDatabase)
if err != nil {
t.Fatalf("unable to create Cloud SQL connection pool: %s", err)
}
@@ -119,20 +120,20 @@ func TestCloudSQLPgSimpleToolEndpoints(t *testing.T) {
tableNameTemplateParam := "template_param_table_" + strings.ReplaceAll(uuid.New().String(), "-", "")
// set up data for param tool
create_statement1, insert_statement1, tool_statement1, params1 := tests.GetPostgresSQLParamToolInfo(tableNameParam)
teardownTable1 := tests.SetupPostgresSQLTable(t, ctx, pool, create_statement1, insert_statement1, tableNameParam, params1)
createStatement1, insertStatement1, paramToolStatement1, paramToolStatement2, params1 := tests.GetPostgresSQLParamToolInfo(tableNameParam)
teardownTable1 := tests.SetupPostgresSQLTable(t, ctx, pool, createStatement1, insertStatement1, tableNameParam, params1)
defer teardownTable1(t)
// set up data for auth tool
create_statement2, insert_statement2, tool_statement2, params2 := tests.GetPostgresSQLAuthToolInfo(tableNameAuth)
teardownTable2 := tests.SetupPostgresSQLTable(t, ctx, pool, create_statement2, insert_statement2, tableNameAuth, params2)
createStatement2, insertStatement2, authToolStatement, params2 := tests.GetPostgresSQLAuthToolInfo(tableNameAuth)
teardownTable2 := tests.SetupPostgresSQLTable(t, ctx, pool, createStatement2, insertStatement2, tableNameAuth, params2)
defer teardownTable2(t)
// Write config into a file and pass it to command
toolsFile := tests.GetToolsConfig(sourceConfig, CLOUD_SQL_POSTGRES_TOOL_KIND, tool_statement1, tool_statement2)
toolsFile := tests.GetToolsConfig(sourceConfig, CloudSQLPostgresToolKind, paramToolStatement1, paramToolStatement2, authToolStatement)
toolsFile = tests.AddPgExecuteSqlConfig(t, toolsFile)
tmplSelectCombined, tmplSelectFilterCombined := tests.GetPostgresSQLTmplToolStatement()
toolsFile = tests.AddTemplateParamConfig(t, toolsFile, CLOUD_SQL_POSTGRES_TOOL_KIND, tmplSelectCombined, tmplSelectFilterCombined, "")
toolsFile = tests.AddTemplateParamConfig(t, toolsFile, CloudSQLPostgresToolKind, tmplSelectCombined, tmplSelectFilterCombined, "")
cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...)
if err != nil {
@@ -142,7 +143,7 @@ func TestCloudSQLPgSimpleToolEndpoints(t *testing.T) {
waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
out, err := cmd.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`))
out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out)
if err != nil {
t.Logf("toolbox command logs: \n%s", out)
t.Fatalf("toolbox didn't start successfully: %s", err)
@@ -151,8 +152,8 @@ func TestCloudSQLPgSimpleToolEndpoints(t *testing.T) {
tests.RunToolGetTest(t)
select1Want, failInvocationWant, createTableStatement := tests.GetPostgresWants()
invokeParamWant, mcpInvokeParamWant := tests.GetNonSpannerInvokeParamWant()
tests.RunToolInvokeTest(t, select1Want, invokeParamWant)
invokeParamWant, invokeParamWantNull, mcpInvokeParamWant := tests.GetNonSpannerInvokeParamWant()
tests.RunToolInvokeTest(t, select1Want, invokeParamWant, invokeParamWantNull)
tests.RunExecuteSqlToolInvokeTest(t, createTableStatement, select1Want)
tests.RunMCPToolCallMethod(t, mcpInvokeParamWant, failInvocationWant)
tests.RunToolInvokeWithTemplateParameters(t, tableNameTemplateParam, tests.NewTemplateParameterTestConfig())
@@ -178,7 +179,7 @@ func TestCloudSQLPgIpConnection(t *testing.T) {
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
sourceConfig["ipType"] = tc.ipType
err := tests.RunSourceConnectionTest(t, sourceConfig, CLOUD_SQL_POSTGRES_TOOL_KIND)
err := tests.RunSourceConnectionTest(t, sourceConfig, CloudSQLPostgresToolKind)
if err != nil {
t.Fatalf("Connection test failure: %s", err)
}
@@ -189,32 +190,32 @@ func TestCloudSQLPgIpConnection(t *testing.T) {
func TestCloudSQLPgIAMConnection(t *testing.T) {
getCloudSQLPgVars(t)
// service account email used for IAM should trim the suffix
serviceAccountEmail := strings.TrimSuffix(tests.SERVICE_ACCOUNT_EMAIL, ".gserviceaccount.com")
serviceAccountEmail := strings.TrimSuffix(tests.ServiceAccountEmail, ".gserviceaccount.com")
noPassSourceConfig := map[string]any{
"kind": CLOUD_SQL_POSTGRES_SOURCE_KIND,
"project": CLOUD_SQL_POSTGRES_PROJECT,
"instance": CLOUD_SQL_POSTGRES_INSTANCE,
"region": CLOUD_SQL_POSTGRES_REGION,
"database": CLOUD_SQL_POSTGRES_DATABASE,
"kind": CloudSQLPostgresSourceKind,
"project": CloudSQLPostgresProject,
"instance": CloudSQLPostgresInstance,
"region": CloudSQLPostgresRegion,
"database": CloudSQLPostgresDatabase,
"user": serviceAccountEmail,
}
noUserSourceConfig := map[string]any{
"kind": CLOUD_SQL_POSTGRES_SOURCE_KIND,
"project": CLOUD_SQL_POSTGRES_PROJECT,
"instance": CLOUD_SQL_POSTGRES_INSTANCE,
"region": CLOUD_SQL_POSTGRES_REGION,
"database": CLOUD_SQL_POSTGRES_DATABASE,
"kind": CloudSQLPostgresSourceKind,
"project": CloudSQLPostgresProject,
"instance": CloudSQLPostgresInstance,
"region": CloudSQLPostgresRegion,
"database": CloudSQLPostgresDatabase,
"password": "random",
}
noUserNoPassSourceConfig := map[string]any{
"kind": CLOUD_SQL_POSTGRES_SOURCE_KIND,
"project": CLOUD_SQL_POSTGRES_PROJECT,
"instance": CLOUD_SQL_POSTGRES_INSTANCE,
"region": CLOUD_SQL_POSTGRES_REGION,
"database": CLOUD_SQL_POSTGRES_DATABASE,
"kind": CloudSQLPostgresSourceKind,
"project": CloudSQLPostgresProject,
"instance": CloudSQLPostgresInstance,
"region": CloudSQLPostgresRegion,
"database": CloudSQLPostgresDatabase,
}
tcs := []struct {
name string
@@ -239,7 +240,7 @@ func TestCloudSQLPgIAMConnection(t *testing.T) {
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
err := tests.RunSourceConnectionTest(t, tc.sourceConfig, CLOUD_SQL_POSTGRES_TOOL_KIND)
err := tests.RunSourceConnectionTest(t, tc.sourceConfig, CloudSQLPostgresToolKind)
if err != nil {
if tc.isErr {
return

View File

@@ -28,7 +28,7 @@ import (
)
// GetToolsConfig returns a mock tools config file
func GetToolsConfig(sourceConfig map[string]any, toolKind, param_tool_statement, auth_tool_statement string) map[string]any {
func GetToolsConfig(sourceConfig map[string]any, toolKind, paramToolStatement, paramToolStatement2, authToolStatement string) map[string]any {
// Write config into a file and pass it to command
toolsFile := map[string]any{
"sources": map[string]any{
@@ -51,7 +51,7 @@ func GetToolsConfig(sourceConfig map[string]any, toolKind, param_tool_statement,
"kind": toolKind,
"source": "my-instance",
"description": "Tool to test invocation with params.",
"statement": param_tool_statement,
"statement": paramToolStatement,
"parameters": []any{
map[string]any{
"name": "id",
@@ -65,12 +65,25 @@ func GetToolsConfig(sourceConfig map[string]any, toolKind, param_tool_statement,
},
},
},
"my-param-tool2": map[string]any{
"kind": toolKind,
"source": "my-instance",
"description": "Tool to test invocation with params.",
"statement": paramToolStatement2,
"parameters": []any{
map[string]any{
"name": "id",
"type": "integer",
"description": "user ID",
},
},
},
"my-auth-tool": map[string]any{
"kind": toolKind,
"source": "my-instance",
"description": "Tool to test authenticated parameters.",
// statement to auto-fill authenticated parameter
"statement": auth_tool_statement,
"statement": authToolStatement,
"parameters": []map[string]any{
{
"name": "email",
@@ -237,8 +250,8 @@ func AddMySqlExecuteSqlConfig(t *testing.T, config map[string]any) map[string]an
return config
}
// AddMssqlExecuteSqlConfig gets the tools config for `mssql-execute-sql`
func AddMssqlExecuteSqlConfig(t *testing.T, config map[string]any) map[string]any {
// AddMSSQLExecuteSqlConfig gets the tools config for `mssql-execute-sql`
func AddMSSQLExecuteSqlConfig(t *testing.T, config map[string]any) map[string]any {
tools, ok := config["tools"].(map[string]any)
if !ok {
t.Fatalf("unable to get tools from config")
@@ -261,21 +274,22 @@ func AddMssqlExecuteSqlConfig(t *testing.T, config map[string]any) map[string]an
}
// GetPostgresSQLParamToolInfo returns statements and param for my-param-tool postgres-sql kind
func GetPostgresSQLParamToolInfo(tableName string) (string, string, string, []any) {
create_statement := fmt.Sprintf("CREATE TABLE %s (id SERIAL PRIMARY KEY, name TEXT);", tableName)
insert_statement := fmt.Sprintf("INSERT INTO %s (name) VALUES ($1), ($2), ($3);", tableName)
tool_statement := fmt.Sprintf("SELECT * FROM %s WHERE id = $1 OR name = $2;", tableName)
params := []any{"Alice", "Jane", "Sid"}
return create_statement, insert_statement, tool_statement, params
func GetPostgresSQLParamToolInfo(tableName string) (string, string, string, string, []any) {
createStatement := fmt.Sprintf("CREATE TABLE %s (id SERIAL PRIMARY KEY, name TEXT);", tableName)
insertStatement := fmt.Sprintf("INSERT INTO %s (name) VALUES ($1), ($2), ($3), ($4);", tableName)
toolStatement := fmt.Sprintf("SELECT * FROM %s WHERE id = $1 OR name = $2;", tableName)
toolStatement2 := fmt.Sprintf("SELECT * FROM %s WHERE id = $1;", tableName)
params := []any{"Alice", "Jane", "Sid", nil}
return createStatement, insertStatement, toolStatement, toolStatement2, params
}
// GetPostgresSQLAuthToolInfo returns statements and param of my-auth-tool for postgres-sql kind
func GetPostgresSQLAuthToolInfo(tableName string) (string, string, string, []any) {
create_statement := fmt.Sprintf("CREATE TABLE %s (id SERIAL PRIMARY KEY, name TEXT, email TEXT);", tableName)
insert_statement := fmt.Sprintf("INSERT INTO %s (name, email) VALUES ($1, $2), ($3, $4)", tableName)
tool_statement := fmt.Sprintf("SELECT name FROM %s WHERE email = $1;", tableName)
params := []any{"Alice", SERVICE_ACCOUNT_EMAIL, "Jane", "janedoe@gmail.com"}
return create_statement, insert_statement, tool_statement, params
createStatement := fmt.Sprintf("CREATE TABLE %s (id SERIAL PRIMARY KEY, name TEXT, email TEXT);", tableName)
insertStatement := fmt.Sprintf("INSERT INTO %s (name, email) VALUES ($1, $2), ($3, $4)", tableName)
toolStatement := fmt.Sprintf("SELECT name FROM %s WHERE email = $1;", tableName)
params := []any{"Alice", ServiceAccountEmail, "Jane", "janedoe@gmail.com"}
return createStatement, insertStatement, toolStatement, params
}
// GetPostgresSQLTmplToolStatement returns statements and param for template parameter test cases for postgres-sql kind
@@ -285,60 +299,63 @@ func GetPostgresSQLTmplToolStatement() (string, string) {
return tmplSelectCombined, tmplSelectFilterCombined
}
// GetMssqlParamToolInfo returns statements and param for my-param-tool mssql-sql kind
func GetMssqlParamToolInfo(tableName string) (string, string, string, []any) {
create_statement := fmt.Sprintf("CREATE TABLE %s (id INT IDENTITY(1,1) PRIMARY KEY, name VARCHAR(255));", tableName)
insert_statement := fmt.Sprintf("INSERT INTO %s (name) VALUES (@alice), (@jane), (@sid);", tableName)
tool_statement := fmt.Sprintf("SELECT * FROM %s WHERE id = @id OR name = @p2;", tableName)
params := []any{sql.Named("alice", "Alice"), sql.Named("jane", "Jane"), sql.Named("sid", "Sid")}
return create_statement, insert_statement, tool_statement, params
// GetMSSQLParamToolInfo returns statements and param for my-param-tool mssql-sql kind
func GetMSSQLParamToolInfo(tableName string) (string, string, string, string, []any) {
createStatement := fmt.Sprintf("CREATE TABLE %s (id INT IDENTITY(1,1) PRIMARY KEY, name VARCHAR(255));", tableName)
insertStatement := fmt.Sprintf("INSERT INTO %s (name) VALUES (@alice), (@jane), (@sid), (@nil);", tableName)
toolStatement := fmt.Sprintf("SELECT * FROM %s WHERE id = @id OR name = @p2;", tableName)
toolStatement2 := fmt.Sprintf("SELECT * FROM %s WHERE id = @id;", tableName)
params := []any{sql.Named("alice", "Alice"), sql.Named("jane", "Jane"), sql.Named("sid", "Sid"), sql.Named("nil", nil)}
return createStatement, insertStatement, toolStatement, toolStatement2, params
}
// GetMssqlAuthToolInfo returns statements and param of my-auth-tool for mssql-sql kind
func GetMssqlAuthToolInfo(tableName string) (string, string, string, []any) {
create_statement := fmt.Sprintf("CREATE TABLE %s (id INT IDENTITY(1,1) PRIMARY KEY, name VARCHAR(255), email VARCHAR(255));", tableName)
insert_statement := fmt.Sprintf("INSERT INTO %s (name, email) VALUES (@alice, @aliceemail), (@jane, @janeemail);", tableName)
tool_statement := fmt.Sprintf("SELECT name FROM %s WHERE email = @email;", tableName)
params := []any{sql.Named("alice", "Alice"), sql.Named("aliceemail", SERVICE_ACCOUNT_EMAIL), sql.Named("jane", "Jane"), sql.Named("janeemail", "janedoe@gmail.com")}
return create_statement, insert_statement, tool_statement, params
// GetMSSQLAuthToolInfo returns statements and param of my-auth-tool for mssql-sql kind
func GetMSSQLAuthToolInfo(tableName string) (string, string, string, []any) {
createStatement := fmt.Sprintf("CREATE TABLE %s (id INT IDENTITY(1,1) PRIMARY KEY, name VARCHAR(255), email VARCHAR(255));", tableName)
insertStatement := fmt.Sprintf("INSERT INTO %s (name, email) VALUES (@alice, @aliceemail), (@jane, @janeemail);", tableName)
toolStatement := fmt.Sprintf("SELECT name FROM %s WHERE email = @email;", tableName)
params := []any{sql.Named("alice", "Alice"), sql.Named("aliceemail", ServiceAccountEmail), sql.Named("jane", "Jane"), sql.Named("janeemail", "janedoe@gmail.com")}
return createStatement, insertStatement, toolStatement, params
}
// GetMssqlTmplToolStatement returns statements and param for template parameter test cases for mysql-sql kind
func GetMssqlTmplToolStatement() (string, string) {
// GetMSSQLTmplToolStatement returns statements and param for template parameter test cases for mysql-sql kind
func GetMSSQLTmplToolStatement() (string, string) {
tmplSelectCombined := "SELECT * FROM {{.tableName}} WHERE id = @id"
tmplSelectFilterCombined := "SELECT * FROM {{.tableName}} WHERE {{.columnFilter}} = @name"
return tmplSelectCombined, tmplSelectFilterCombined
}
// GetMysqlParamToolInfo returns statements and param for my-param-tool mysql-sql kind
func GetMysqlParamToolInfo(tableName string) (string, string, string, []any) {
create_statement := fmt.Sprintf("CREATE TABLE %s (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255));", tableName)
insert_statement := fmt.Sprintf("INSERT INTO %s (name) VALUES (?), (?), (?);", tableName)
tool_statement := fmt.Sprintf("SELECT * FROM %s WHERE id = ? OR name = ?;", tableName)
params := []any{"Alice", "Jane", "Sid"}
return create_statement, insert_statement, tool_statement, params
// GetMySQLParamToolInfo returns statements and param for my-param-tool mysql-sql kind
func GetMySQLParamToolInfo(tableName string) (string, string, string, string, []any) {
createStatement := fmt.Sprintf("CREATE TABLE %s (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255));", tableName)
insertStatement := fmt.Sprintf("INSERT INTO %s (name) VALUES (?), (?), (?), (?);", tableName)
toolStatement := fmt.Sprintf("SELECT * FROM %s WHERE id = ? OR name = ?;", tableName)
toolStatement2 := fmt.Sprintf("SELECT * FROM %s WHERE id = ?;", tableName)
params := []any{"Alice", "Jane", "Sid", nil}
return createStatement, insertStatement, toolStatement, toolStatement2, params
}
// GetMysqlAuthToolInfo returns statements and param of my-auth-tool for mysql-sql kind
func GetMysqlAuthToolInfo(tableName string) (string, string, string, []any) {
create_statement := fmt.Sprintf("CREATE TABLE %s (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255), email VARCHAR(255));", tableName)
insert_statement := fmt.Sprintf("INSERT INTO %s (name, email) VALUES (?, ?), (?, ?)", tableName)
tool_statement := fmt.Sprintf("SELECT name FROM %s WHERE email = ?;", tableName)
params := []any{"Alice", SERVICE_ACCOUNT_EMAIL, "Jane", "janedoe@gmail.com"}
return create_statement, insert_statement, tool_statement, params
// GetMySQLAuthToolInfo returns statements and param of my-auth-tool for mysql-sql kind
func GetMySQLAuthToolInfo(tableName string) (string, string, string, []any) {
createStatement := fmt.Sprintf("CREATE TABLE %s (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255), email VARCHAR(255));", tableName)
insertStatement := fmt.Sprintf("INSERT INTO %s (name, email) VALUES (?, ?), (?, ?)", tableName)
toolStatement := fmt.Sprintf("SELECT name FROM %s WHERE email = ?;", tableName)
params := []any{"Alice", ServiceAccountEmail, "Jane", "janedoe@gmail.com"}
return createStatement, insertStatement, toolStatement, params
}
// GetMysqlTmplToolStatement returns statements and param for template parameter test cases for mysql-sql kind
func GetMysqlTmplToolStatement() (string, string) {
// GetMySQLTmplToolStatement returns statements and param for template parameter test cases for mysql-sql kind
func GetMySQLTmplToolStatement() (string, string) {
tmplSelectCombined := "SELECT * FROM {{.tableName}} WHERE id = ?"
tmplSelectFilterCombined := "SELECT * FROM {{.tableName}} WHERE {{.columnFilter}} = ?"
return tmplSelectCombined, tmplSelectFilterCombined
}
func GetNonSpannerInvokeParamWant() (string, string) {
func GetNonSpannerInvokeParamWant() (string, string, string) {
invokeParamWant := "[{\"id\":1,\"name\":\"Alice\"},{\"id\":3,\"name\":\"Sid\"}]"
invokeParamWantNull := "[{\"id\":4,\"name\":null}]"
mcpInvokeParamWant := `{"jsonrpc":"2.0","id":"my-param-tool","result":{"content":[{"type":"text","text":"{\"id\":1,\"name\":\"Alice\"}"},{"type":"text","text":"{\"id\":3,\"name\":\"Sid\"}"}]}}`
return invokeParamWant, mcpInvokeParamWant
return invokeParamWant, invokeParamWantNull, mcpInvokeParamWant
}
// GetPostgresWants return the expected wants for postgres
@@ -349,16 +366,16 @@ func GetPostgresWants() (string, string, string) {
return select1Want, failInvocationWant, createTableStatement
}
// GetMssqlWants return the expected wants for mssql
func GetMssqlWants() (string, string, string) {
// GetMSSQLWants return the expected wants for mssql
func GetMSSQLWants() (string, string, string) {
select1Want := "[{\"\":1}]"
failInvocationWant := `{"jsonrpc":"2.0","id":"invoke-fail-tool","result":{"content":[{"type":"text","text":"unable to execute query: mssql: Could not find stored procedure 'SELEC'."}],"isError":true}}`
createTableStatement := `"CREATE TABLE t (id INT IDENTITY(1,1) PRIMARY KEY, name NVARCHAR(MAX))"`
return select1Want, failInvocationWant, createTableStatement
}
// GetMysqlWants return the expected wants for mysql
func GetMysqlWants() (string, string, string) {
// GetMySQLWants return the expected wants for mysql
func GetMySQLWants() (string, string, string) {
select1Want := "[{\"1\":1}]"
failInvocationWant := `{"jsonrpc":"2.0","id":"invoke-fail-tool","result":{"content":[{"type":"text","text":"unable to execute query: Error 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'SELEC 1' at line 1"}],"isError":true}}`
createTableStatement := `"CREATE TABLE t (id SERIAL PRIMARY KEY, name TEXT)"`
@@ -367,20 +384,20 @@ func GetMysqlWants() (string, string, string) {
// SetupPostgresSQLTable creates and inserts data into a table of tool
// compatible with postgres-sql tool
func SetupPostgresSQLTable(t *testing.T, ctx context.Context, pool *pgxpool.Pool, create_statement, insert_statement, tableName string, params []any) func(*testing.T) {
func SetupPostgresSQLTable(t *testing.T, ctx context.Context, pool *pgxpool.Pool, createStatement, insertStatement, tableName string, params []any) func(*testing.T) {
err := pool.Ping(ctx)
if err != nil {
t.Fatalf("unable to connect to test database: %s", err)
}
// Create table
_, err = pool.Query(ctx, create_statement)
_, err = pool.Query(ctx, createStatement)
if err != nil {
t.Fatalf("unable to create test table %s: %s", tableName, err)
}
// Insert test data
_, err = pool.Query(ctx, insert_statement, params...)
_, err = pool.Query(ctx, insertStatement, params...)
if err != nil {
t.Fatalf("unable to insert test data: %s", err)
}
@@ -396,20 +413,20 @@ func SetupPostgresSQLTable(t *testing.T, ctx context.Context, pool *pgxpool.Pool
// SetupMsSQLTable creates and inserts data into a table of tool
// compatible with mssql-sql tool
func SetupMsSQLTable(t *testing.T, ctx context.Context, pool *sql.DB, create_statement, insert_statement, tableName string, params []any) func(*testing.T) {
func SetupMsSQLTable(t *testing.T, ctx context.Context, pool *sql.DB, createStatement, insertStatement, tableName string, params []any) func(*testing.T) {
err := pool.PingContext(ctx)
if err != nil {
t.Fatalf("unable to connect to test database: %s", err)
}
// Create table
_, err = pool.QueryContext(ctx, create_statement)
_, err = pool.QueryContext(ctx, createStatement)
if err != nil {
t.Fatalf("unable to create test table %s: %s", tableName, err)
}
// Insert test data
_, err = pool.QueryContext(ctx, insert_statement, params...)
_, err = pool.QueryContext(ctx, insertStatement, params...)
if err != nil {
t.Fatalf("unable to insert test data: %s", err)
}
@@ -425,20 +442,20 @@ func SetupMsSQLTable(t *testing.T, ctx context.Context, pool *sql.DB, create_sta
// SetupMySQLTable creates and inserts data into a table of tool
// compatible with mysql-sql tool
func SetupMySQLTable(t *testing.T, ctx context.Context, pool *sql.DB, create_statement, insert_statement, tableName string, params []any) func(*testing.T) {
func SetupMySQLTable(t *testing.T, ctx context.Context, pool *sql.DB, createStatement, insertStatement, tableName string, params []any) func(*testing.T) {
err := pool.PingContext(ctx)
if err != nil {
t.Fatalf("unable to connect to test database: %s", err)
}
// Create table
_, err = pool.QueryContext(ctx, create_statement)
_, err = pool.QueryContext(ctx, createStatement)
if err != nil {
t.Fatalf("unable to create test table %s: %s", tableName, err)
}
// Insert test data
_, err = pool.QueryContext(ctx, insert_statement, params...)
_, err = pool.QueryContext(ctx, insertStatement, params...)
if err != nil {
t.Fatalf("unable to insert test data: %s", err)
}
@@ -453,12 +470,13 @@ func SetupMySQLTable(t *testing.T, ctx context.Context, pool *sql.DB, create_sta
}
// GetRedisWants return the expected wants for redis
func GetRedisValkeyWants() (string, string, string, string) {
func GetRedisValkeyWants() (string, string, string, string, string) {
select1Want := "[\"PONG\"]"
failInvocationWant := `unknown command 'SELEC 1;', with args beginning with: \""}]}}`
invokeParamWant := "[{\"id\":\"1\",\"name\":\"Alice\"},{\"id\":\"3\",\"name\":\"Sid\"}]"
invokeParamWantNull := `[{"id":"4","name":""}]`
mcpInvokeParamWant := `{"jsonrpc":"2.0","id":"my-param-tool","result":{"content":[{"type":"text","text":"{\"id\":\"1\",\"name\":\"Alice\"}"},{"type":"text","text":"{\"id\":\"3\",\"name\":\"Sid\"}"}]}}`
return select1Want, failInvocationWant, invokeParamWant, mcpInvokeParamWant
return select1Want, failInvocationWant, invokeParamWant, invokeParamWantNull, mcpInvokeParamWant
}
func GetRedisValkeyToolsConfig(sourceConfig map[string]any, toolKind string) map[string]any {
@@ -497,6 +515,19 @@ func GetRedisValkeyToolsConfig(sourceConfig map[string]any, toolKind string) map
},
},
},
"my-param-tool2": map[string]any{
"kind": toolKind,
"source": "my-instance",
"description": "Tool to test invocation with params.",
"commands": [][]string{{"HGETALL", "row4"}},
"parameters": []any{
map[string]any{
"name": "id",
"type": "integer",
"description": "user ID",
},
},
},
"my-auth-tool": map[string]any{
"kind": toolKind,
"source": "my-instance",
@@ -534,7 +565,5 @@ func GetRedisValkeyToolsConfig(sourceConfig map[string]any, toolKind string) map
},
},
}
return toolsFile
}

View File

@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package tests
package couchbase
import (
"context"
@@ -25,6 +25,7 @@ import (
"github.com/couchbase/gocb/v2"
"github.com/google/uuid"
"github.com/googleapis/genai-toolbox/internal/testutils"
"github.com/googleapis/genai-toolbox/tests"
)
@@ -34,12 +35,11 @@ const (
)
var (
couchbaseConnection = os.Getenv("COUCHBASE_CONNECTION")
couchbaseBucket = os.Getenv("COUCHBASE_BUCKET")
couchbaseScope = os.Getenv("COUCHBASE_SCOPE")
couchbaseUser = os.Getenv("COUCHBASE_USER")
couchbasePass = os.Getenv("COUCHBASE_PASS")
SERVICE_ACCOUNT_EMAIL = os.Getenv("SERVICE_ACCOUNT_EMAIL")
couchbaseConnection = os.Getenv("COUCHBASE_CONNECTION")
couchbaseBucket = os.Getenv("COUCHBASE_BUCKET")
couchbaseScope = os.Getenv("COUCHBASE_SCOPE")
couchbaseUser = os.Getenv("COUCHBASE_USER")
couchbasePass = os.Getenv("COUCHBASE_PASS")
)
// getCouchbaseVars validates and returns Couchbase configuration variables
@@ -103,7 +103,7 @@ func TestCouchbaseToolEndpoints(t *testing.T) {
collectionNameTemplateParam := "template_param_" + strings.ReplaceAll(uuid.New().String(), "-", "")
// Set up data for param tool
paramToolStatement, params1 := getCouchbaseParamToolInfo(collectionNameParam)
paramToolStatement1, paramToolStatement2, params1 := getCouchbaseParamToolInfo(collectionNameParam)
teardownCollection1 := setupCouchbaseCollection(t, ctx, cluster, couchbaseBucket, couchbaseScope, collectionNameParam, params1)
defer teardownCollection1(t)
@@ -118,7 +118,7 @@ func TestCouchbaseToolEndpoints(t *testing.T) {
defer teardownCollection3(t)
// Write config into a file and pass it to command
toolsFile := tests.GetToolsConfig(sourceConfig, couchbaseToolKind, paramToolStatement, authToolStatement)
toolsFile := tests.GetToolsConfig(sourceConfig, couchbaseToolKind, paramToolStatement1, paramToolStatement2, authToolStatement)
toolsFile = tests.AddTemplateParamConfig(t, toolsFile, couchbaseToolKind, tmplSelectCombined, tmplSelectFilterCombined, tmplSelectAll)
cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...)
@@ -129,7 +129,7 @@ func TestCouchbaseToolEndpoints(t *testing.T) {
waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
out, err := cmd.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`))
out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out)
if err != nil {
t.Logf("toolbox command logs: \n%s", out)
t.Fatalf("toolbox didn't start successfully: %s", err)
@@ -140,8 +140,8 @@ func TestCouchbaseToolEndpoints(t *testing.T) {
select1Want := "[{\"$1\":1}]"
failMcpInvocationWant := "{\"jsonrpc\":\"2.0\",\"id\":\"invoke-fail-tool\",\"result\":{\"content\":[{\"type\":\"text\",\"text\":\"unable to execute query: parsing failure | {\\\"statement\\\":\\\"SELEC 1;\\\""
invokeParamWant, mcpInvokeParamWant := tests.GetNonSpannerInvokeParamWant()
tests.RunToolInvokeTest(t, select1Want, invokeParamWant)
invokeParamWant, invokeParamWantNull, mcpInvokeParamWant := tests.GetNonSpannerInvokeParamWant()
tests.RunToolInvokeTest(t, select1Want, invokeParamWant, invokeParamWantNull)
tests.RunMCPToolCallMethod(t, mcpInvokeParamWant, failMcpInvocationWant)
templateParamTestConfig := tests.NewTemplateParameterTestConfig(
@@ -231,18 +231,22 @@ func setupCouchbaseCollection(t *testing.T, ctx context.Context, cluster *gocb.C
}
// getCouchbaseParamToolInfo returns statements and params for my-param-tool couchbase-sql kind
func getCouchbaseParamToolInfo(collectionName string) (string, []map[string]any) {
func getCouchbaseParamToolInfo(collectionName string) (string, string, []map[string]any) {
// N1QL uses positional or named parameters with $ prefix
toolStatement := fmt.Sprintf("SELECT TONUMBER(meta().id) as id, "+
"%s.* FROM %s WHERE meta().id = TOSTRING($id) OR name = $name order by meta().id",
collectionName, collectionName)
toolStatement2 := fmt.Sprintf("SELECT TONUMBER(meta().id) as id, "+
"%s.* FROM %s WHERE meta().id = TOSTRING($id) order by meta().id",
collectionName, collectionName)
params := []map[string]any{
{"name": "Alice"},
{"name": "Jane"},
{"name": "Sid"},
{"name": nil},
}
return toolStatement, params
return toolStatement, toolStatement2, params
}
// getCouchbaseAuthToolInfo returns statements and param of my-auth-tool for couchbase-sql kind
@@ -250,7 +254,7 @@ func getCouchbaseAuthToolInfo(collectionName string) (string, []map[string]any)
toolStatement := fmt.Sprintf("SELECT name FROM %s WHERE email = $email", collectionName)
params := []map[string]any{
{"name": "Alice", "email": SERVICE_ACCOUNT_EMAIL},
{"name": "Alice", "email": tests.ServiceAccountEmail},
{"name": "Jane", "email": "janedoe@gmail.com"},
}
return toolStatement, params

View File

@@ -26,24 +26,24 @@ import (
"testing"
"time"
"github.com/googleapis/genai-toolbox/internal/testutils"
"github.com/googleapis/genai-toolbox/tests"
)
var (
DGRAPH_SOURCE_KIND = "dgraph"
DGRAPH_TOOL_KIND = "dgraph-dql"
DGRAPH_API_KEY = "api-key"
DGRAPH_URL = os.Getenv("DGRAPH_URL")
DgraphSourceKind = "dgraph"
DgraphApiKey = "api-key"
DgraphUrl = os.Getenv("DGRAPH_URL")
)
func getDgraphVars(t *testing.T) map[string]any {
if DGRAPH_URL == "" {
if DgraphUrl == "" {
t.Fatal("'DGRAPH_URL' not set")
}
return map[string]any{
"kind": DGRAPH_SOURCE_KIND,
"dgraphUrl": DGRAPH_URL,
"apiKey": DGRAPH_API_KEY,
"kind": DgraphSourceKind,
"dgraphUrl": DgraphUrl,
"apiKey": DgraphApiKey,
}
}
@@ -79,7 +79,7 @@ func TestDgraphToolEndpoints(t *testing.T) {
waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
out, err := cmd.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`))
out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out)
if err != nil {
t.Logf("toolbox command logs: \n%s", out)
t.Fatalf("toolbox didn't start successfully: %s", err)

View File

@@ -28,13 +28,14 @@ import (
"testing"
"time"
"github.com/googleapis/genai-toolbox/internal/testutils"
"github.com/googleapis/genai-toolbox/internal/tools"
"github.com/googleapis/genai-toolbox/tests"
)
var (
HTTP_SOURCE_KIND = "http"
HTTP_TOOL_KIND = "http"
HttpSourceKind = "http"
HttpToolKind = "http"
)
func getHTTPSourceConfig(t *testing.T) map[string]any {
@@ -44,7 +45,7 @@ func getHTTPSourceConfig(t *testing.T) map[string]any {
}
idToken = "Bearer " + idToken
return map[string]any{
"kind": HTTP_SOURCE_KIND,
"kind": HttpSourceKind,
"headers": map[string]string{"Authorization": idToken},
}
}
@@ -59,6 +60,8 @@ func multiTool(w http.ResponseWriter, r *http.Request) {
handleTool0(w, r)
case "tool1":
handleTool1(w, r)
case "tool1a":
handleTool1a(w, r)
case "tool2":
handleTool2(w, r)
case "tool3":
@@ -130,6 +133,27 @@ func handleTool1(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
// handler function for the test server
func handleTool1a(w http.ResponseWriter, r *http.Request) {
// expect GET method
if r.Method != http.MethodGet {
errorMessage := fmt.Sprintf("expected GET method but got: %s", string(r.Method))
http.Error(w, errorMessage, http.StatusBadRequest)
return
}
id := r.URL.Query().Get("id")
if id == "4" {
response := `[{"id":4,"name":null}]`
_, err := w.Write([]byte(response))
if err != nil {
http.Error(w, "Failed to write response", http.StatusInternalServerError)
}
return
}
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
// handler function for the test server
func handleTool2(w http.ResponseWriter, r *http.Request) {
// expect GET method
@@ -245,7 +269,7 @@ func TestHttpToolEndpoints(t *testing.T) {
var args []string
toolsFile := getHTTPToolsConfig(sourceConfig, HTTP_TOOL_KIND)
toolsFile := getHTTPToolsConfig(sourceConfig, HttpToolKind)
cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...)
if err != nil {
t.Fatalf("command initialization returned an error: %s", err)
@@ -254,16 +278,16 @@ func TestHttpToolEndpoints(t *testing.T) {
waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
out, err := cmd.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`))
out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out)
if err != nil {
t.Logf("toolbox command logs: \n%s", out)
t.Fatalf("toolbox didn't start successfully: %s", err)
}
select1Want := `["Hello","World"]`
invokeParamWant, _ := tests.GetNonSpannerInvokeParamWant()
invokeParamWant, invokeParamWantNull, _ := tests.GetNonSpannerInvokeParamWant()
tests.RunToolGetTest(t)
tests.RunToolInvokeTest(t, select1Want, invokeParamWant)
tests.RunToolInvokeTest(t, select1Want, invokeParamWant, invokeParamWantNull)
runAdvancedHTTPInvokeTest(t)
}
@@ -383,6 +407,16 @@ func getHTTPToolsConfig(sourceConfig map[string]any, toolKind string) map[string
"bodyParams": []tools.Parameter{tools.NewStringParameter("name", "user name")},
"headers": map[string]string{"Content-Type": "application/json"},
},
"my-param-tool2": map[string]any{
"kind": toolKind,
"source": "my-instance",
"method": "GET",
"path": "/tool1a",
"description": "some description",
"queryParams": []tools.Parameter{
tools.NewIntParameter("id", "user ID")},
"headers": map[string]string{"Content-Type": "application/json"},
},
"my-auth-tool": map[string]any{
"kind": toolKind,
"source": "my-instance",

View File

@@ -26,45 +26,46 @@ import (
"time"
"github.com/google/uuid"
"github.com/googleapis/genai-toolbox/internal/testutils"
"github.com/googleapis/genai-toolbox/tests"
)
var (
MSSQL_SOURCE_KIND = "mssql"
MSSQL_TOOL_KIND = "mssql-sql"
MSSQL_DATABASE = os.Getenv("MSSQL_DATABASE")
MSSQL_HOST = os.Getenv("MSSQL_HOST")
MSSQL_PORT = os.Getenv("MSSQL_PORT")
MSSQL_USER = os.Getenv("MSSQL_USER")
MSSQL_PASS = os.Getenv("MSSQL_PASS")
MSSQLSourceKind = "mssql"
MSSQLToolKind = "mssql-sql"
MSSQLDatabase = os.Getenv("MSSQL_DATABASE")
MSSQLHost = os.Getenv("MSSQL_HOST")
MSSQLPort = os.Getenv("MSSQL_PORT")
MSSQLUser = os.Getenv("MSSQL_USER")
MSSQLPass = os.Getenv("MSSQL_PASS")
)
func getMsSQLVars(t *testing.T) map[string]any {
switch "" {
case MSSQL_DATABASE:
case MSSQLDatabase:
t.Fatal("'MSSQL_DATABASE' not set")
case MSSQL_HOST:
case MSSQLHost:
t.Fatal("'MSSQL_HOST' not set")
case MSSQL_PORT:
case MSSQLPort:
t.Fatal("'MSSQL_PORT' not set")
case MSSQL_USER:
case MSSQLUser:
t.Fatal("'MSSQL_USER' not set")
case MSSQL_PASS:
case MSSQLPass:
t.Fatal("'MSSQL_PASS' not set")
}
return map[string]any{
"kind": MSSQL_SOURCE_KIND,
"host": MSSQL_HOST,
"port": MSSQL_PORT,
"database": MSSQL_DATABASE,
"user": MSSQL_USER,
"password": MSSQL_PASS,
"kind": MSSQLSourceKind,
"host": MSSQLHost,
"port": MSSQLPort,
"database": MSSQLDatabase,
"user": MSSQLUser,
"password": MSSQLPass,
}
}
// Copied over from mssql.go
func initMssqlConnection(host, port, user, pass, dbname string) (*sql.DB, error) {
func initMSSQLConnection(host, port, user, pass, dbname string) (*sql.DB, error) {
// Create dsn
query := url.Values{}
query.Add("database", dbname)
@@ -83,14 +84,14 @@ func initMssqlConnection(host, port, user, pass, dbname string) (*sql.DB, error)
return db, nil
}
func TestMssqlToolEndpoints(t *testing.T) {
func TestMSSQLToolEndpoints(t *testing.T) {
sourceConfig := getMsSQLVars(t)
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
var args []string
pool, err := initMssqlConnection(MSSQL_HOST, MSSQL_PORT, MSSQL_USER, MSSQL_PASS, MSSQL_DATABASE)
pool, err := initMSSQLConnection(MSSQLHost, MSSQLPort, MSSQLUser, MSSQLPass, MSSQLDatabase)
if err != nil {
t.Fatalf("unable to create SQL Server connection pool: %s", err)
}
@@ -101,20 +102,20 @@ func TestMssqlToolEndpoints(t *testing.T) {
tableNameTemplateParam := "template_param_table_" + strings.ReplaceAll(uuid.New().String(), "-", "")
// set up data for param tool
create_statement1, insert_statement1, tool_statement1, params1 := tests.GetMssqlParamToolInfo(tableNameParam)
teardownTable1 := tests.SetupMsSQLTable(t, ctx, pool, create_statement1, insert_statement1, tableNameParam, params1)
createStatement1, insertStatement1, paramToolStatement1, paramToolStatement2, params1 := tests.GetMSSQLParamToolInfo(tableNameParam)
teardownTable1 := tests.SetupMsSQLTable(t, ctx, pool, createStatement1, insertStatement1, tableNameParam, params1)
defer teardownTable1(t)
// set up data for auth tool
create_statement2, insert_statement2, tool_statement2, params2 := tests.GetMssqlAuthToolInfo(tableNameAuth)
teardownTable2 := tests.SetupMsSQLTable(t, ctx, pool, create_statement2, insert_statement2, tableNameAuth, params2)
createStatement2, insertStatement2, authToolStatement, params2 := tests.GetMSSQLAuthToolInfo(tableNameAuth)
teardownTable2 := tests.SetupMsSQLTable(t, ctx, pool, createStatement2, insertStatement2, tableNameAuth, params2)
defer teardownTable2(t)
// Write config into a file and pass it to command
toolsFile := tests.GetToolsConfig(sourceConfig, MSSQL_TOOL_KIND, tool_statement1, tool_statement2)
toolsFile = tests.AddMssqlExecuteSqlConfig(t, toolsFile)
tmplSelectCombined, tmplSelectFilterCombined := tests.GetMssqlTmplToolStatement()
toolsFile = tests.AddTemplateParamConfig(t, toolsFile, MSSQL_TOOL_KIND, tmplSelectCombined, tmplSelectFilterCombined, "")
toolsFile := tests.GetToolsConfig(sourceConfig, MSSQLToolKind, paramToolStatement1, paramToolStatement2, authToolStatement)
toolsFile = tests.AddMSSQLExecuteSqlConfig(t, toolsFile)
tmplSelectCombined, tmplSelectFilterCombined := tests.GetMSSQLTmplToolStatement()
toolsFile = tests.AddTemplateParamConfig(t, toolsFile, MSSQLToolKind, tmplSelectCombined, tmplSelectFilterCombined, "")
cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...)
if err != nil {
@@ -124,7 +125,7 @@ func TestMssqlToolEndpoints(t *testing.T) {
waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
out, err := cmd.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`))
out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out)
if err != nil {
t.Logf("toolbox command logs: \n%s", out)
t.Fatalf("toolbox didn't start successfully: %s", err)
@@ -132,9 +133,9 @@ func TestMssqlToolEndpoints(t *testing.T) {
tests.RunToolGetTest(t)
select1Want, failInvocationWant, createTableStatement := tests.GetMssqlWants()
invokeParamWant, mcpInvokeParamWant := tests.GetNonSpannerInvokeParamWant()
tests.RunToolInvokeTest(t, select1Want, invokeParamWant)
select1Want, failInvocationWant, createTableStatement := tests.GetMSSQLWants()
invokeParamWant, invokeParamWantNull, mcpInvokeParamWant := tests.GetNonSpannerInvokeParamWant()
tests.RunToolInvokeTest(t, select1Want, invokeParamWant, invokeParamWantNull)
tests.RunExecuteSqlToolInvokeTest(t, createTableStatement, select1Want)
tests.RunMCPToolCallMethod(t, mcpInvokeParamWant, failInvocationWant)
tests.RunToolInvokeWithTemplateParameters(t, tableNameTemplateParam, tests.NewTemplateParameterTestConfig())

View File

@@ -25,40 +25,41 @@ import (
"time"
"github.com/google/uuid"
"github.com/googleapis/genai-toolbox/internal/testutils"
"github.com/googleapis/genai-toolbox/tests"
)
var (
MYSQL_SOURCE_KIND = "mysql"
MYSQL_TOOL_KIND = "mysql-sql"
MYSQL_DATABASE = os.Getenv("MYSQL_DATABASE")
MYSQL_HOST = os.Getenv("MYSQL_HOST")
MYSQL_PORT = os.Getenv("MYSQL_PORT")
MYSQL_USER = os.Getenv("MYSQL_USER")
MYSQL_PASS = os.Getenv("MYSQL_PASS")
MySQLSourceKind = "mysql"
MySQLToolKind = "mysql-sql"
MySQLDatabase = os.Getenv("MYSQL_DATABASE")
MySQLHost = os.Getenv("MYSQL_HOST")
MySQLPort = os.Getenv("MYSQL_PORT")
MySQLUser = os.Getenv("MYSQL_USER")
MySQLPass = os.Getenv("MYSQL_PASS")
)
func getMySQLVars(t *testing.T) map[string]any {
switch "" {
case MYSQL_DATABASE:
case MySQLDatabase:
t.Fatal("'MYSQL_DATABASE' not set")
case MYSQL_HOST:
case MySQLHost:
t.Fatal("'MYSQL_HOST' not set")
case MYSQL_PORT:
case MySQLPort:
t.Fatal("'MYSQL_PORT' not set")
case MYSQL_USER:
case MySQLUser:
t.Fatal("'MYSQL_USER' not set")
case MYSQL_PASS:
case MySQLPass:
t.Fatal("'MYSQL_PASS' not set")
}
return map[string]any{
"kind": MYSQL_SOURCE_KIND,
"host": MYSQL_HOST,
"port": MYSQL_PORT,
"database": MYSQL_DATABASE,
"user": MYSQL_USER,
"password": MYSQL_PASS,
"kind": MySQLSourceKind,
"host": MySQLHost,
"port": MySQLPort,
"database": MySQLDatabase,
"user": MySQLUser,
"password": MySQLPass,
}
}
@@ -74,14 +75,14 @@ func initMySQLConnectionPool(host, port, user, pass, dbname string) (*sql.DB, er
return pool, nil
}
func TestMysqlToolEndpoints(t *testing.T) {
func TestMySQLToolEndpoints(t *testing.T) {
sourceConfig := getMySQLVars(t)
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
var args []string
pool, err := initMySQLConnectionPool(MYSQL_HOST, MYSQL_PORT, MYSQL_USER, MYSQL_PASS, MYSQL_DATABASE)
pool, err := initMySQLConnectionPool(MySQLHost, MySQLPort, MySQLUser, MySQLPass, MySQLDatabase)
if err != nil {
t.Fatalf("unable to create MySQL connection pool: %s", err)
}
@@ -92,20 +93,20 @@ func TestMysqlToolEndpoints(t *testing.T) {
tableNameTemplateParam := "template_param_table_" + strings.ReplaceAll(uuid.New().String(), "-", "")
// set up data for param tool
create_statement1, insert_statement1, tool_statement1, params1 := tests.GetMysqlParamToolInfo(tableNameParam)
teardownTable1 := tests.SetupMySQLTable(t, ctx, pool, create_statement1, insert_statement1, tableNameParam, params1)
createStatement1, insertStatement1, paramToolStatement1, paramToolStatement2, params1 := tests.GetMySQLParamToolInfo(tableNameParam)
teardownTable1 := tests.SetupMySQLTable(t, ctx, pool, createStatement1, insertStatement1, tableNameParam, params1)
defer teardownTable1(t)
// set up data for auth tool
create_statement2, insert_statement2, tool_statement2, params2 := tests.GetMysqlAuthToolInfo(tableNameAuth)
teardownTable2 := tests.SetupMySQLTable(t, ctx, pool, create_statement2, insert_statement2, tableNameAuth, params2)
createStatement2, insertStatement2, authToolStatement, params2 := tests.GetMySQLAuthToolInfo(tableNameAuth)
teardownTable2 := tests.SetupMySQLTable(t, ctx, pool, createStatement2, insertStatement2, tableNameAuth, params2)
defer teardownTable2(t)
// Write config into a file and pass it to command
toolsFile := tests.GetToolsConfig(sourceConfig, MYSQL_TOOL_KIND, tool_statement1, tool_statement2)
toolsFile := tests.GetToolsConfig(sourceConfig, MySQLToolKind, paramToolStatement1, paramToolStatement2, authToolStatement)
toolsFile = tests.AddMySqlExecuteSqlConfig(t, toolsFile)
tmplSelectCombined, tmplSelectFilterCombined := tests.GetMysqlTmplToolStatement()
toolsFile = tests.AddTemplateParamConfig(t, toolsFile, MYSQL_TOOL_KIND, tmplSelectCombined, tmplSelectFilterCombined, "")
tmplSelectCombined, tmplSelectFilterCombined := tests.GetMySQLTmplToolStatement()
toolsFile = tests.AddTemplateParamConfig(t, toolsFile, MySQLToolKind, tmplSelectCombined, tmplSelectFilterCombined, "")
cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...)
if err != nil {
@@ -115,7 +116,7 @@ func TestMysqlToolEndpoints(t *testing.T) {
waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
out, err := cmd.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`))
out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out)
if err != nil {
t.Logf("toolbox command logs: \n%s", out)
t.Fatalf("toolbox didn't start successfully: %s", err)
@@ -123,9 +124,9 @@ func TestMysqlToolEndpoints(t *testing.T) {
tests.RunToolGetTest(t)
select1Want, failInvocationWant, createTableStatement := tests.GetMysqlWants()
invokeParamWant, mcpInvokeParamWant := tests.GetNonSpannerInvokeParamWant()
tests.RunToolInvokeTest(t, select1Want, invokeParamWant)
select1Want, failInvocationWant, createTableStatement := tests.GetMySQLWants()
invokeParamWant, invokeParamWantNull, mcpInvokeParamWant := tests.GetNonSpannerInvokeParamWant()
tests.RunToolInvokeTest(t, select1Want, invokeParamWant, invokeParamWantNull)
tests.RunExecuteSqlToolInvokeTest(t, createTableStatement, select1Want)
tests.RunMCPToolCallMethod(t, mcpInvokeParamWant, failInvocationWant)
tests.RunToolInvokeWithTemplateParameters(t, tableNameTemplateParam, tests.NewTemplateParameterTestConfig())

View File

@@ -26,36 +26,36 @@ import (
"testing"
"time"
"github.com/googleapis/genai-toolbox/internal/testutils"
"github.com/googleapis/genai-toolbox/tests"
)
var (
NEO4J_SOURCE_KIND = "neo4j"
NEO4J_TOOL_KIND = "neo4j-cypher"
NEO4J_DATABASE = os.Getenv("NEO4J_DATABASE")
NEO4J_URI = os.Getenv("NEO4J_URI")
NEO4J_USER = os.Getenv("NEO4J_USER")
NEO4J_PASS = os.Getenv("NEO4J_PASS")
Neo4jSourceKind = "neo4j"
Neo4jDatabase = os.Getenv("NEO4J_DATABASE")
Neo4jUri = os.Getenv("NEO4J_URI")
Neo4jUser = os.Getenv("NEO4J_USER")
Neo4jPass = os.Getenv("NEO4J_PASS")
)
func getNeo4jVars(t *testing.T) map[string]any {
switch "" {
case NEO4J_DATABASE:
case Neo4jDatabase:
t.Fatal("'NEO4J_DATABASE' not set")
case NEO4J_URI:
case Neo4jUri:
t.Fatal("'NEO4J_URI' not set")
case NEO4J_USER:
case Neo4jUser:
t.Fatal("'NEO4J_USER' not set")
case NEO4J_PASS:
case Neo4jPass:
t.Fatal("'NEO4J_PASS' not set")
}
return map[string]any{
"kind": NEO4J_SOURCE_KIND,
"uri": NEO4J_URI,
"database": NEO4J_DATABASE,
"user": NEO4J_USER,
"password": NEO4J_PASS,
"kind": Neo4jSourceKind,
"uri": Neo4jUri,
"database": Neo4jDatabase,
"user": Neo4jUser,
"password": Neo4jPass,
}
}
@@ -88,7 +88,7 @@ func TestNeo4jToolEndpoints(t *testing.T) {
waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
out, err := cmd.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`))
out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out)
if err != nil {
t.Logf("toolbox command logs: \n%s", out)
t.Fatalf("toolbox didn't start successfully: %s", err)

View File

@@ -25,41 +25,42 @@ import (
"time"
"github.com/google/uuid"
"github.com/googleapis/genai-toolbox/internal/testutils"
"github.com/googleapis/genai-toolbox/tests"
"github.com/jackc/pgx/v5/pgxpool"
)
var (
POSTGRES_SOURCE_KIND = "postgres"
POSTGRES_TOOL_KIND = "postgres-sql"
POSTGRES_DATABASE = os.Getenv("POSTGRES_DATABASE")
POSTGRES_HOST = os.Getenv("POSTGRES_HOST")
POSTGRES_PORT = os.Getenv("POSTGRES_PORT")
POSTGRES_USER = os.Getenv("POSTGRES_USER")
POSTGRES_PASS = os.Getenv("POSTGRES_PASS")
PostgresSourceKind = "postgres"
PostgresToolKind = "postgres-sql"
PostgresDatabase = os.Getenv("POSTGRES_DATABASE")
PostgresHost = os.Getenv("POSTGRES_HOST")
PostgresPort = os.Getenv("POSTGRES_PORT")
PostgresUser = os.Getenv("POSTGRES_USER")
PostgresPass = os.Getenv("POSTGRES_PASS")
)
func getPostgresVars(t *testing.T) map[string]any {
switch "" {
case POSTGRES_DATABASE:
case PostgresDatabase:
t.Fatal("'POSTGRES_DATABASE' not set")
case POSTGRES_HOST:
case PostgresHost:
t.Fatal("'POSTGRES_HOST' not set")
case POSTGRES_PORT:
case PostgresPort:
t.Fatal("'POSTGRES_PORT' not set")
case POSTGRES_USER:
case PostgresUser:
t.Fatal("'POSTGRES_USER' not set")
case POSTGRES_PASS:
case PostgresPass:
t.Fatal("'POSTGRES_PASS' not set")
}
return map[string]any{
"kind": POSTGRES_SOURCE_KIND,
"host": POSTGRES_HOST,
"port": POSTGRES_PORT,
"database": POSTGRES_DATABASE,
"user": POSTGRES_USER,
"password": POSTGRES_PASS,
"kind": PostgresSourceKind,
"host": PostgresHost,
"port": PostgresPort,
"database": PostgresDatabase,
"user": PostgresUser,
"password": PostgresPass,
}
}
@@ -87,7 +88,7 @@ func TestPostgres(t *testing.T) {
var args []string
pool, err := initPostgresConnectionPool(POSTGRES_HOST, POSTGRES_PORT, POSTGRES_USER, POSTGRES_PASS, POSTGRES_DATABASE)
pool, err := initPostgresConnectionPool(PostgresHost, PostgresPort, PostgresUser, PostgresPass, PostgresDatabase)
if err != nil {
t.Fatalf("unable to create postgres connection pool: %s", err)
}
@@ -98,20 +99,20 @@ func TestPostgres(t *testing.T) {
tableNameTemplateParam := "template_param_table_" + strings.ReplaceAll(uuid.New().String(), "-", "")
// set up data for param tool
create_statement1, insert_statement1, tool_statement1, params1 := tests.GetPostgresSQLParamToolInfo(tableNameParam)
teardownTable1 := tests.SetupPostgresSQLTable(t, ctx, pool, create_statement1, insert_statement1, tableNameParam, params1)
createStatement1, insertStatement1, paramToolStatement1, paramToolStatement2, params1 := tests.GetPostgresSQLParamToolInfo(tableNameParam)
teardownTable1 := tests.SetupPostgresSQLTable(t, ctx, pool, createStatement1, insertStatement1, tableNameParam, params1)
defer teardownTable1(t)
// set up data for auth tool
create_statement2, insert_statement2, tool_statement2, params2 := tests.GetPostgresSQLAuthToolInfo(tableNameAuth)
teardownTable2 := tests.SetupPostgresSQLTable(t, ctx, pool, create_statement2, insert_statement2, tableNameAuth, params2)
createStatement2, insertStatement2, authToolStatement, params2 := tests.GetPostgresSQLAuthToolInfo(tableNameAuth)
teardownTable2 := tests.SetupPostgresSQLTable(t, ctx, pool, createStatement2, insertStatement2, tableNameAuth, params2)
defer teardownTable2(t)
// Write config into a file and pass it to command
toolsFile := tests.GetToolsConfig(sourceConfig, POSTGRES_TOOL_KIND, tool_statement1, tool_statement2)
toolsFile := tests.GetToolsConfig(sourceConfig, PostgresToolKind, paramToolStatement1, paramToolStatement2, authToolStatement)
toolsFile = tests.AddPgExecuteSqlConfig(t, toolsFile)
tmplSelectCombined, tmplSelectFilterCombined := tests.GetPostgresSQLTmplToolStatement()
toolsFile = tests.AddTemplateParamConfig(t, toolsFile, POSTGRES_TOOL_KIND, tmplSelectCombined, tmplSelectFilterCombined, "")
toolsFile = tests.AddTemplateParamConfig(t, toolsFile, PostgresToolKind, tmplSelectCombined, tmplSelectFilterCombined, "")
cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...)
if err != nil {
@@ -121,7 +122,7 @@ func TestPostgres(t *testing.T) {
waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
out, err := cmd.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`))
out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out)
if err != nil {
t.Logf("toolbox command logs: \n%s", out)
t.Fatalf("toolbox didn't start successfully: %s", err)
@@ -130,8 +131,8 @@ func TestPostgres(t *testing.T) {
tests.RunToolGetTest(t)
select1Want, failInvocationWant, createTableStatement := tests.GetPostgresWants()
invokeParamWant, mcpInvokeParamWant := tests.GetNonSpannerInvokeParamWant()
tests.RunToolInvokeTest(t, select1Want, invokeParamWant)
invokeParamWant, invokeParamWantNull, mcpInvokeParamWant := tests.GetNonSpannerInvokeParamWant()
tests.RunToolInvokeTest(t, select1Want, invokeParamWant, invokeParamWantNull)
tests.RunExecuteSqlToolInvokeTest(t, createTableStatement, select1Want)
tests.RunMCPToolCallMethod(t, mcpInvokeParamWant, failInvocationWant)
tests.RunToolInvokeWithTemplateParameters(t, tableNameTemplateParam, tests.NewTemplateParameterTestConfig())

View File

@@ -22,28 +22,29 @@ import (
"testing"
"time"
"github.com/googleapis/genai-toolbox/internal/testutils"
"github.com/googleapis/genai-toolbox/tests"
"github.com/redis/go-redis/v9"
)
var (
REDIS_SOURCE_KIND = "redis"
REDIS_TOOL_KIND = "redis"
REDIS_ADDRESS = os.Getenv("REDIS_ADDRESS")
REDIS_PASS = os.Getenv("REDIS_PASS")
RedisSourceKind = "redis"
RedisToolKind = "redis"
RedisAddress = os.Getenv("REDIS_ADDRESS")
RedisPass = os.Getenv("REDIS_PASS")
)
func getRedisVars(t *testing.T) map[string]any {
switch "" {
case REDIS_ADDRESS:
case RedisAddress:
t.Fatal("'REDIS_ADDRESS' not set")
case REDIS_PASS:
case RedisPass:
t.Fatal("'REDIS_PASS' not set")
}
return map[string]any{
"kind": REDIS_SOURCE_KIND,
"address": []string{REDIS_ADDRESS},
"password": REDIS_PASS,
"kind": RedisSourceKind,
"address": []string{RedisAddress},
"password": RedisPass,
}
}
@@ -70,7 +71,7 @@ func TestRedisToolEndpoints(t *testing.T) {
var args []string
client, err := initRedisClient(ctx, REDIS_ADDRESS, REDIS_PASS)
client, err := initRedisClient(ctx, RedisAddress, RedisPass)
if err != nil {
t.Fatalf("unable to create Redis connection: %s", err)
}
@@ -80,7 +81,7 @@ func TestRedisToolEndpoints(t *testing.T) {
defer teardownDB(t)
// Write config into a file and pass it to command
toolsFile := tests.GetRedisValkeyToolsConfig(sourceConfig, REDIS_TOOL_KIND)
toolsFile := tests.GetRedisValkeyToolsConfig(sourceConfig, RedisToolKind)
cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...)
if err != nil {
@@ -90,7 +91,7 @@ func TestRedisToolEndpoints(t *testing.T) {
waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
out, err := cmd.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`))
out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out)
if err != nil {
t.Logf("toolbox command logs: \n%s", out)
t.Fatalf("toolbox didn't start successfully: %s", err)
@@ -98,18 +99,19 @@ func TestRedisToolEndpoints(t *testing.T) {
tests.RunToolGetTest(t)
select1Want, failInvocationWant, invokeParamWant, mcpInvokeParamWant := tests.GetRedisValkeyWants()
tests.RunToolInvokeTest(t, select1Want, invokeParamWant)
select1Want, failInvocationWant, invokeParamWant, invokeParamWantNull, mcpInvokeParamWant := tests.GetRedisValkeyWants()
tests.RunToolInvokeTest(t, select1Want, invokeParamWant, invokeParamWantNull)
tests.RunMCPToolCallMethod(t, mcpInvokeParamWant, failInvocationWant)
}
func setupRedisDB(t *testing.T, ctx context.Context, client *redis.Client) func(*testing.T) {
keys := []string{"row1", "row2", "row3"}
keys := []string{"row1", "row2", "row3", "row4"}
commands := [][]any{
{"HSET", keys[0], "id", 1, "name", "Alice"},
{"HSET", keys[1], "id", 2, "name", "Jane"},
{"HSET", keys[2], "id", 3, "name", "Sid"},
{"HSET", tests.SERVICE_ACCOUNT_EMAIL, "name", "Alice"},
{"HSET", keys[3], "id", 4, "name", nil},
{"HSET", tests.ServiceAccountEmail, "name", "Alice"},
}
for _, c := range commands {
resp := client.Do(ctx, c...)

View File

@@ -15,13 +15,10 @@
package tests
import (
"bufio"
"context"
"fmt"
"io"
"os"
"regexp"
"strings"
yaml "github.com/goccy/go-yaml"
@@ -133,63 +130,3 @@ func (c *CmdExec) Close() {
c.Close()
}
}
// WaitForString waits until the server logs a single line that matches the provided regex.
// returns the output of whatever the server sent so far.
func (c *CmdExec) WaitForString(ctx context.Context, re *regexp.Regexp) (string, error) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
in := bufio.NewReader(c.Out)
// read lines in background, sending result of each read over a channel
// this allows us to use in.ReadString without blocking
type result struct {
s string
err error
}
output := make(chan result)
go func() {
defer close(output)
for {
select {
case <-ctx.Done():
// if the context is canceled, the orig thread will send back the error
// so we can just exit the goroutine here
return
default:
// otherwise read a line from the output
s, err := in.ReadString('\n')
if err != nil {
output <- result{err: err}
return
}
output <- result{s: s}
// if that last string matched, exit the goroutine
if re.MatchString(s) {
return
}
}
}
}()
// collect the output until the ctx is canceled, an error was hit,
// or match was found (which is indicated the channel is closed)
var sb strings.Builder
for {
select {
case <-ctx.Done():
// if ctx is done, return that error
return sb.String(), ctx.Err()
case o, ok := <-output:
if !ok {
// match was found!
return sb.String(), nil
}
if o.err != nil {
// error was found!
return sb.String(), o.err
}
sb.WriteString(o.s)
}
}
}

View File

@@ -26,6 +26,7 @@ import (
"time"
"cloud.google.com/go/cloudsqlconn"
"github.com/googleapis/genai-toolbox/internal/testutils"
)
// RunSourceConnection test for source connection
@@ -57,7 +58,7 @@ func RunSourceConnectionTest(t *testing.T, sourceConfig map[string]any, toolKind
waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
out, err := cmd.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`))
out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out)
if err != nil {
t.Logf("toolbox command logs: \n%s", out)
return fmt.Errorf("toolbox didn't start successfully: %s", err)

View File

@@ -31,33 +31,34 @@ import (
database "cloud.google.com/go/spanner/admin/database/apiv1"
"cloud.google.com/go/spanner/admin/database/apiv1/databasepb"
"github.com/google/uuid"
"github.com/googleapis/genai-toolbox/internal/testutils"
"github.com/googleapis/genai-toolbox/internal/tools"
"github.com/googleapis/genai-toolbox/tests"
)
var (
SPANNER_SOURCE_KIND = "spanner"
SPANNER_TOOL_KIND = "spanner-sql"
SPANNER_PROJECT = os.Getenv("SPANNER_PROJECT")
SPANNER_DATABASE = os.Getenv("SPANNER_DATABASE")
SPANNER_INSTANCE = os.Getenv("SPANNER_INSTANCE")
SpannerSourceKind = "spanner"
SpannerToolKind = "spanner-sql"
SpannerProject = os.Getenv("SPANNER_PROJECT")
SpannerDatabase = os.Getenv("SPANNER_DATABASE")
SpannerInstance = os.Getenv("SPANNER_INSTANCE")
)
func getSpannerVars(t *testing.T) map[string]any {
switch "" {
case SPANNER_PROJECT:
case SpannerProject:
t.Fatal("'SPANNER_PROJECT' not set")
case SPANNER_DATABASE:
case SpannerDatabase:
t.Fatal("'SPANNER_DATABASE' not set")
case SPANNER_INSTANCE:
case SpannerInstance:
t.Fatal("'SPANNER_INSTANCE' not set")
}
return map[string]any{
"kind": SPANNER_SOURCE_KIND,
"project": SPANNER_PROJECT,
"instance": SPANNER_INSTANCE,
"database": SPANNER_DATABASE,
"kind": SpannerSourceKind,
"project": SpannerProject,
"instance": SpannerInstance,
"database": SpannerDatabase,
}
}
@@ -90,13 +91,13 @@ func initSpannerClients(ctx context.Context, project, instance, dbname string) (
func TestSpannerToolEndpoints(t *testing.T) {
sourceConfig := getSpannerVars(t)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
var args []string
// Create Spanner client
dataClient, adminClient, err := initSpannerClients(ctx, SPANNER_PROJECT, SPANNER_INSTANCE, SPANNER_DATABASE)
dataClient, adminClient, err := initSpannerClients(ctx, SpannerProject, SpannerInstance, SpannerDatabase)
if err != nil {
t.Fatalf("unable to create Spanner client: %s", err)
}
@@ -107,19 +108,19 @@ func TestSpannerToolEndpoints(t *testing.T) {
tableNameTemplateParam := "template_param_table_" + strings.ReplaceAll(uuid.New().String(), "-", "")
// set up data for param tool
create_statement1, insert_statement1, tool_statement1, params1 := getSpannerParamToolInfo(tableNameParam)
createStatement1, insertStatement1, paramToolStatement1, paramToolStatement2, params1 := getSpannerParamToolInfo(tableNameParam)
dbString := fmt.Sprintf(
"projects/%s/instances/%s/databases/%s",
SPANNER_PROJECT,
SPANNER_INSTANCE,
SPANNER_DATABASE,
SpannerProject,
SpannerInstance,
SpannerDatabase,
)
teardownTable1 := setupSpannerTable(t, ctx, adminClient, dataClient, create_statement1, insert_statement1, tableNameParam, dbString, params1)
teardownTable1 := setupSpannerTable(t, ctx, adminClient, dataClient, createStatement1, insertStatement1, tableNameParam, dbString, params1)
defer teardownTable1(t)
// set up data for auth tool
create_statement2, insert_statement2, tool_statement2, params2 := getSpannerAuthToolInfo(tableNameAuth)
teardownTable2 := setupSpannerTable(t, ctx, adminClient, dataClient, create_statement2, insert_statement2, tableNameAuth, dbString, params2)
createStatement2, insertStatement2, authToolStatement, params2 := getSpannerAuthToolInfo(tableNameAuth)
teardownTable2 := setupSpannerTable(t, ctx, adminClient, dataClient, createStatement2, insertStatement2, tableNameAuth, dbString, params2)
defer teardownTable2(t)
// set up data for template param tool
@@ -128,7 +129,7 @@ func TestSpannerToolEndpoints(t *testing.T) {
defer teardownTableTmpl(t)
// Write config into a file and pass it to command
toolsFile := tests.GetToolsConfig(sourceConfig, SPANNER_TOOL_KIND, tool_statement1, tool_statement2)
toolsFile := tests.GetToolsConfig(sourceConfig, SpannerToolKind, paramToolStatement1, paramToolStatement2, authToolStatement)
toolsFile = addSpannerExecuteSqlConfig(t, toolsFile)
toolsFile = addSpannerReadOnlyConfig(t, toolsFile)
toolsFile = addTemplateParamConfig(t, toolsFile)
@@ -141,7 +142,7 @@ func TestSpannerToolEndpoints(t *testing.T) {
waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
out, err := cmd.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`))
out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out)
if err != nil {
t.Logf("toolbox command logs: \n%s", out)
t.Fatalf("toolbox didn't start successfully: %s", err)
@@ -152,10 +153,11 @@ func TestSpannerToolEndpoints(t *testing.T) {
select1Want := "[{\"\":\"1\"}]"
accessSchemaWant := "[{\"schema_name\":\"INFORMATION_SCHEMA\"}]"
invokeParamWant := "[{\"id\":\"1\",\"name\":\"Alice\"},{\"id\":\"3\",\"name\":\"Sid\"}]"
invokeParamWantNull := `[{"id":"4","name":null}]`
mcpInvokeParamWant := `{"jsonrpc":"2.0","id":"my-param-tool","result":{"content":[{"type":"text","text":"{\"id\":\"1\",\"name\":\"Alice\"}"},{"type":"text","text":"{\"id\":\"3\",\"name\":\"Sid\"}"}]}}`
failInvocationWant := `"jsonrpc":"2.0","id":"invoke-fail-tool","result":{"content":[{"type":"text","text":"unable to execute client: unable to parse row: spanner: code = \"InvalidArgument\", desc = \"Syntax error: Unexpected identifier \\\\\\\"SELEC\\\\\\\" [at 1:1]\\\\nSELEC 1;\\\\n^\"`
tests.RunToolInvokeTest(t, select1Want, invokeParamWant)
tests.RunToolInvokeTest(t, select1Want, invokeParamWant, invokeParamWantNull)
tests.RunMCPToolCallMethod(t, mcpInvokeParamWant, failInvocationWant)
runSpannerSchemaToolInvokeTest(t, accessSchemaWant)
runSpannerExecuteSqlToolInvokeTest(t, select1Want, invokeParamWant, tableNameParam, tableNameAuth)
@@ -169,26 +171,27 @@ func TestSpannerToolEndpoints(t *testing.T) {
}
// getSpannerToolInfo returns statements and param for my-param-tool for spanner-sql kind
func getSpannerParamToolInfo(tableName string) (string, string, string, map[string]any) {
create_statement := fmt.Sprintf("CREATE TABLE %s (id INT64, name STRING(MAX)) PRIMARY KEY (id)", tableName)
insert_statement := fmt.Sprintf("INSERT INTO %s (id, name) VALUES (1, @name1), (2, @name2), (3, @name3)", tableName)
tool_statement := fmt.Sprintf("SELECT * FROM %s WHERE id = @id OR name = @name", tableName)
params := map[string]any{"name1": "Alice", "name2": "Jane", "name3": "Sid"}
return create_statement, insert_statement, tool_statement, params
func getSpannerParamToolInfo(tableName string) (string, string, string, string, map[string]any) {
createStatement := fmt.Sprintf("CREATE TABLE %s (id INT64, name STRING(MAX)) PRIMARY KEY (id)", tableName)
insertStatement := fmt.Sprintf("INSERT INTO %s (id, name) VALUES (1, @name1), (2, @name2), (3, @name3), (4, @name4)", tableName)
toolStatement := fmt.Sprintf("SELECT * FROM %s WHERE id = @id OR name = @name", tableName)
toolStatement2 := fmt.Sprintf("SELECT * FROM %s WHERE id = @id", tableName)
params := map[string]any{"name1": "Alice", "name2": "Jane", "name3": "Sid", "name4": nil}
return createStatement, insertStatement, toolStatement, toolStatement2, params
}
// getSpannerAuthToolInfo returns statements and param of my-auth-tool for spanner-sql kind
func getSpannerAuthToolInfo(tableName string) (string, string, string, map[string]any) {
create_statement := fmt.Sprintf("CREATE TABLE %s (id INT64, name STRING(MAX), email STRING(MAX)) PRIMARY KEY (id)", tableName)
insert_statement := fmt.Sprintf("INSERT INTO %s (id, name, email) VALUES (1, @name1, @email1), (2, @name2, @email2)", tableName)
tool_statement := fmt.Sprintf("SELECT name FROM %s WHERE email = @email", tableName)
createStatement := fmt.Sprintf("CREATE TABLE %s (id INT64, name STRING(MAX), email STRING(MAX)) PRIMARY KEY (id)", tableName)
insertStatement := fmt.Sprintf("INSERT INTO %s (id, name, email) VALUES (1, @name1, @email1), (2, @name2, @email2)", tableName)
toolStatement := fmt.Sprintf("SELECT name FROM %s WHERE email = @email", tableName)
params := map[string]any{
"name1": "Alice",
"email1": tests.SERVICE_ACCOUNT_EMAIL,
"email1": tests.ServiceAccountEmail,
"name2": "Jane",
"email2": "janedoe@gmail.com",
}
return create_statement, insert_statement, tool_statement, params
return createStatement, insertStatement, toolStatement, params
}
// setupSpannerTable creates and inserts data into a table of tool
@@ -230,13 +233,13 @@ func setupSpannerTable(t *testing.T, ctx context.Context, adminClient *database.
Statements: []string{fmt.Sprintf("DROP TABLE %s", tableName)},
})
if err != nil {
t.Errorf("unable to start drop table operation: %s", err)
t.Errorf("unable to start drop %s operation: %s", tableName, err)
return
}
err = op.Wait(ctx)
if err != nil {
t.Errorf("Teardown failed: %s", err)
opErr := op.Wait(ctx)
if opErr != nil {
t.Errorf("Teardown failed: %s", opErr)
}
}
}
@@ -352,7 +355,7 @@ func addTemplateParamConfig(t *testing.T, config map[string]any) map[string]any
return config
}
func runSpannerExecuteSqlToolInvokeTest(t *testing.T, select_1_want, invokeParamWant, tableNameParam, tableNameAuth string) {
func runSpannerExecuteSqlToolInvokeTest(t *testing.T, select1Want, invokeParamWant, tableNameParam, tableNameAuth string) {
// Get ID token
idToken, err := tests.GetGoogleIdToken(tests.ClientId)
if err != nil {
@@ -373,7 +376,7 @@ func runSpannerExecuteSqlToolInvokeTest(t *testing.T, select_1_want, invokeParam
api: "http://127.0.0.1:5000/api/tool/my-exec-sql-tool-read-only/invoke",
requestHeader: map[string]string{},
requestBody: bytes.NewBuffer([]byte(`{"sql":"SELECT 1"}`)),
want: select_1_want,
want: select1Want,
isErr: false,
},
{
@@ -417,7 +420,7 @@ func runSpannerExecuteSqlToolInvokeTest(t *testing.T, select_1_want, invokeParam
api: "http://127.0.0.1:5000/api/tool/my-exec-sql-tool/invoke",
requestHeader: map[string]string{},
requestBody: bytes.NewBuffer([]byte(`{"sql":"SELECT 1"}`)),
want: select_1_want,
want: select1Want,
isErr: false,
},
{
@@ -438,7 +441,7 @@ func runSpannerExecuteSqlToolInvokeTest(t *testing.T, select_1_want, invokeParam
name: "invoke my-exec-sql-tool insert entry",
api: "http://127.0.0.1:5000/api/tool/my-exec-sql-tool/invoke",
requestHeader: map[string]string{},
requestBody: bytes.NewBuffer([]byte(fmt.Sprintf("{\"sql\":\"INSERT INTO %s (id, name) VALUES (4, 'test_name')\"}", tableNameParam))),
requestBody: bytes.NewBuffer([]byte(fmt.Sprintf("{\"sql\":\"INSERT INTO %s (id, name) VALUES (5, 'test_name')\"}", tableNameParam))),
want: "null",
isErr: false,
},
@@ -455,7 +458,7 @@ func runSpannerExecuteSqlToolInvokeTest(t *testing.T, select_1_want, invokeParam
requestHeader: map[string]string{"my-google-auth_token": idToken},
requestBody: bytes.NewBuffer([]byte(`{"sql":"SELECT 1"}`)),
isErr: false,
want: select_1_want,
want: select1Want,
},
{
name: "Invoke my-auth-exec-sql-tool with invalid auth token",

View File

@@ -25,19 +25,20 @@ import (
"time"
"github.com/google/uuid"
"github.com/googleapis/genai-toolbox/internal/testutils"
"github.com/googleapis/genai-toolbox/tests"
)
var (
SQLITE_SOURCE_KIND = "sqlite"
SQLITE_TOOL_KIND = "sqlite-sql"
SQLITE_DATABASE = os.Getenv("SQLITE_DATABASE")
SQLiteSourceKind = "sqlite"
SQLiteToolKind = "sqlite-sql"
SQLiteDatabase = os.Getenv("SQLITE_DATABASE")
)
func getSQLiteVars(t *testing.T) map[string]any {
return map[string]any{
"kind": SQLITE_SOURCE_KIND,
"database": SQLITE_DATABASE,
"kind": SQLiteSourceKind,
"database": SQLiteDatabase,
}
}
@@ -80,20 +81,21 @@ func setupSQLiteTestDB(t *testing.T, ctx context.Context, db *sql.DB, createStat
}
}
func getSQLiteParamToolInfo(tableName string) (string, string, string, []any) {
create_statement := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (id INTEGER PRIMARY KEY, name TEXT);", tableName)
insert_statement := fmt.Sprintf("INSERT INTO %s (name) VALUES (?), (?), (?);", tableName)
tool_statement := fmt.Sprintf("SELECT * FROM %s WHERE id = ? OR name = ?;", tableName)
params := []any{"Alice", "Jane", "Sid"}
return create_statement, insert_statement, tool_statement, params
func getSQLiteParamToolInfo(tableName string) (string, string, string, string, []any) {
createStatement := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (id INTEGER PRIMARY KEY, name TEXT);", tableName)
insertStatement := fmt.Sprintf("INSERT INTO %s (name) VALUES (?), (?), (?), (?);", tableName)
toolStatement := fmt.Sprintf("SELECT * FROM %s WHERE id = ? OR name = ?;", tableName)
toolStatement2 := fmt.Sprintf("SELECT * FROM %s WHERE id = ?;", tableName)
params := []any{"Alice", "Jane", "Sid", nil}
return createStatement, insertStatement, toolStatement, toolStatement2, params
}
func getSQLiteAuthToolInfo(tableName string) (string, string, string, []any) {
create_statement := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (id INTEGER PRIMARY KEY, name TEXT NOT NULL, email TEXT)", tableName)
insert_statement := fmt.Sprintf("INSERT INTO %s (name, email) VALUES (?, ?), (?,?) RETURNING id, name, email;", tableName)
tool_statement := fmt.Sprintf("SELECT name FROM %s WHERE email = ?;", tableName)
params := []any{"Alice", tests.SERVICE_ACCOUNT_EMAIL, "Jane", "janedoe@gmail.com"}
return create_statement, insert_statement, tool_statement, params
createStatement := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (id INTEGER PRIMARY KEY, name TEXT NOT NULL, email TEXT)", tableName)
insertStatement := fmt.Sprintf("INSERT INTO %s (name, email) VALUES (?, ?), (?,?) RETURNING id, name, email;", tableName)
toolStatement := fmt.Sprintf("SELECT name FROM %s WHERE email = ?;", tableName)
params := []any{"Alice", tests.ServiceAccountEmail, "Jane", "janedoe@gmail.com"}
return createStatement, insertStatement, toolStatement, params
}
func getSQLiteTmplToolStatement() (string, string) {
@@ -103,7 +105,7 @@ func getSQLiteTmplToolStatement() (string, string) {
}
func TestSQLiteToolEndpoint(t *testing.T) {
db, teardownDb, sqliteDb, err := initSQLiteDb(t, SQLITE_DATABASE)
db, teardownDb, sqliteDb, err := initSQLiteDb(t, SQLiteDatabase)
if err != nil {
t.Fatal(err)
}
@@ -123,17 +125,17 @@ func TestSQLiteToolEndpoint(t *testing.T) {
tableNameTemplateParam := "template_param_table_" + strings.ReplaceAll(uuid.New().String(), "-", "")
// set up data for param tool
create_statement1, insert_statement1, tool_statement1, params1 := getSQLiteParamToolInfo(tableNameParam)
setupSQLiteTestDB(t, ctx, db, create_statement1, insert_statement1, tableNameParam, params1)
createStatement1, insertStatement1, paramToolStatement1, paramToolStatement2, params1 := getSQLiteParamToolInfo(tableNameParam)
setupSQLiteTestDB(t, ctx, db, createStatement1, insertStatement1, tableNameParam, params1)
// set up data for auth tool
create_statement2, insert_statement2, tool_statement2, params2 := getSQLiteAuthToolInfo(tableNameAuth)
setupSQLiteTestDB(t, ctx, db, create_statement2, insert_statement2, tableNameAuth, params2)
createStatement2, insertStatement2, authToolStatement, params2 := getSQLiteAuthToolInfo(tableNameAuth)
setupSQLiteTestDB(t, ctx, db, createStatement2, insertStatement2, tableNameAuth, params2)
// Write config into a file and pass it to command
toolsFile := tests.GetToolsConfig(sourceConfig, SQLITE_TOOL_KIND, tool_statement1, tool_statement2)
toolsFile := tests.GetToolsConfig(sourceConfig, SQLiteToolKind, paramToolStatement1, paramToolStatement2, authToolStatement)
tmplSelectCombined, tmplSelectFilterCombined := getSQLiteTmplToolStatement()
toolsFile = tests.AddTemplateParamConfig(t, toolsFile, SQLITE_TOOL_KIND, tmplSelectCombined, tmplSelectFilterCombined, "")
toolsFile = tests.AddTemplateParamConfig(t, toolsFile, SQLiteToolKind, tmplSelectCombined, tmplSelectFilterCombined, "")
cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...)
if err != nil {
@@ -143,7 +145,7 @@ func TestSQLiteToolEndpoint(t *testing.T) {
waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
out, err := cmd.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`))
out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out)
if err != nil {
t.Logf("toolbox command logs: \n%s", out)
t.Fatalf("toolbox didn't start successfully: %s", err)
@@ -153,8 +155,8 @@ func TestSQLiteToolEndpoint(t *testing.T) {
select1Want := "[{\"1\":1}]"
failInvocationWant := `{"jsonrpc":"2.0","id":"invoke-fail-tool","result":{"content":[{"type":"text","text":"unable to execute query: SQL logic error: near \"SELEC\": syntax error (1)"}],"isError":true}}`
invokeParamWant, mcpInvokeParamWant := tests.GetNonSpannerInvokeParamWant()
tests.RunToolInvokeTest(t, select1Want, invokeParamWant)
invokeParamWant, invokeParamWantNull, mcpInvokeParamWant := tests.GetNonSpannerInvokeParamWant()
tests.RunToolInvokeTest(t, select1Want, invokeParamWant, invokeParamWantNull)
tests.RunMCPToolCallMethod(t, mcpInvokeParamWant, failInvocationWant)
tests.RunToolInvokeWithTemplateParameters(t, tableNameTemplateParam, tests.NewTemplateParameterTestConfig())
}

View File

@@ -76,7 +76,7 @@ func RunToolGetTest(t *testing.T) {
}
// RunToolInvoke runs the tool invoke endpoint
func RunToolInvokeTest(t *testing.T, select1Want, invokeParamWant string) {
func RunToolInvokeTest(t *testing.T, select1Want, invokeParamWant, invokeParamWantNull string) {
// Get ID token
idToken, err := GetGoogleIdToken(ClientId)
if err != nil {
@@ -109,15 +109,23 @@ func RunToolInvokeTest(t *testing.T, select1Want, invokeParamWant string) {
isErr: false,
},
{
name: "Invoke my-tool without parameters",
api: "http://127.0.0.1:5000/api/tool/my-tool/invoke",
name: "invoke my-param-tool2 with nil response",
api: "http://127.0.0.1:5000/api/tool/my-param-tool2/invoke",
requestHeader: map[string]string{},
requestBody: bytes.NewBuffer([]byte(`{"id": 4}`)),
want: invokeParamWantNull,
isErr: false,
},
{
name: "Invoke my-param-tool without parameters",
api: "http://127.0.0.1:5000/api/tool/my-param-tool/invoke",
requestHeader: map[string]string{},
requestBody: bytes.NewBuffer([]byte(`{}`)),
isErr: true,
},
{
name: "Invoke my-tool with insufficient parameters",
api: "http://127.0.0.1:5000/api/tool/my-tool/invoke",
name: "Invoke my-param-tool with insufficient parameters",
api: "http://127.0.0.1:5000/api/tool/my-param-tool/invoke",
requestHeader: map[string]string{},
requestBody: bytes.NewBuffer([]byte(`{"id": 1}`)),
isErr: true,
@@ -428,7 +436,7 @@ func RunToolInvokeWithTemplateParameters(t *testing.T, tableName string, config
}
}
func RunExecuteSqlToolInvokeTest(t *testing.T, createTableStatement string, select_1_want string) {
func RunExecuteSqlToolInvokeTest(t *testing.T, createTableStatement string, select1Want string) {
// Get ID token
idToken, err := GetGoogleIdToken(ClientId)
if err != nil {
@@ -449,7 +457,7 @@ func RunExecuteSqlToolInvokeTest(t *testing.T, createTableStatement string, sele
api: "http://127.0.0.1:5000/api/tool/my-exec-sql-tool/invoke",
requestHeader: map[string]string{},
requestBody: bytes.NewBuffer([]byte(`{"sql":"SELECT 1"}`)),
want: select_1_want,
want: select1Want,
isErr: false,
},
{
@@ -489,7 +497,7 @@ func RunExecuteSqlToolInvokeTest(t *testing.T, createTableStatement string, sele
requestHeader: map[string]string{"my-google-auth_token": idToken},
requestBody: bytes.NewBuffer([]byte(`{"sql":"SELECT 1"}`)),
isErr: false,
want: select_1_want,
want: select1Want,
},
{
name: "Invoke my-auth-exec-sql-tool with invalid auth token",
@@ -597,7 +605,7 @@ func RunInitialize(t *testing.T, protocolVersion string) string {
}
// RunMCPToolCallMethod runs the tool/call for mcp endpoint
func RunMCPToolCallMethod(t *testing.T, invokeParamWant, fail_invocation_want string) {
func RunMCPToolCallMethod(t *testing.T, invokeParamWant, failInvocationWant string) {
sessionId := RunInitialize(t, "2024-11-05")
header := map[string]string{}
if sessionId != "" {
@@ -715,7 +723,7 @@ func RunMCPToolCallMethod(t *testing.T, invokeParamWant, fail_invocation_want st
"arguments": map[string]any{"id": 1},
},
},
want: fail_invocation_want,
want: failInvocationWant,
},
}
for _, tc := range invokeTcs {

View File

@@ -22,24 +22,25 @@ import (
"testing"
"time"
"github.com/googleapis/genai-toolbox/internal/testutils"
"github.com/googleapis/genai-toolbox/tests"
"github.com/valkey-io/valkey-go"
)
var (
VALKEY_SOURCE_KIND = "valkey"
VALKEY_TOOL_KIND = "valkey"
VALKEY_ADDRESS = os.Getenv("VALKEY_ADDRESS")
ValkeySourceKind = "valkey"
ValkeyToolKind = "valkey"
ValkeyAddress = os.Getenv("VALKEY_ADDRESS")
)
func getValkeyVars(t *testing.T) map[string]any {
switch "" {
case VALKEY_ADDRESS:
case ValkeyAddress:
t.Fatal("'VALKEY_ADDRESS' not set")
}
return map[string]any{
"kind": VALKEY_SOURCE_KIND,
"address": []string{VALKEY_ADDRESS},
"kind": ValkeySourceKind,
"address": []string{ValkeyAddress},
"disableCache": true,
}
}
@@ -73,7 +74,7 @@ func TestValkeyToolEndpoints(t *testing.T) {
var args []string
client, err := initValkeyClient(ctx, []string{VALKEY_ADDRESS})
client, err := initValkeyClient(ctx, []string{ValkeyAddress})
if err != nil {
t.Fatalf("unable to create Valkey connection: %s", err)
}
@@ -83,7 +84,7 @@ func TestValkeyToolEndpoints(t *testing.T) {
defer teardownDB(t)
// Write config into a file and pass it to command
toolsFile := tests.GetRedisValkeyToolsConfig(sourceConfig, VALKEY_TOOL_KIND)
toolsFile := tests.GetRedisValkeyToolsConfig(sourceConfig, ValkeyToolKind)
cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...)
if err != nil {
@@ -93,7 +94,7 @@ func TestValkeyToolEndpoints(t *testing.T) {
waitCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
out, err := cmd.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`))
out, err := testutils.WaitForString(waitCtx, regexp.MustCompile(`Server ready to serve`), cmd.Out)
if err != nil {
t.Logf("toolbox command logs: \n%s", out)
t.Fatalf("toolbox didn't start successfully: %s", err)
@@ -101,18 +102,19 @@ func TestValkeyToolEndpoints(t *testing.T) {
tests.RunToolGetTest(t)
select1Want, failInvocationWant, invokeParamWant, mcpInvokeParamWant := tests.GetRedisValkeyWants()
tests.RunToolInvokeTest(t, select1Want, invokeParamWant)
select1Want, failInvocationWant, invokeParamWant, invokeParamWantNull, mcpInvokeParamWant := tests.GetRedisValkeyWants()
tests.RunToolInvokeTest(t, select1Want, invokeParamWant, invokeParamWantNull)
tests.RunMCPToolCallMethod(t, mcpInvokeParamWant, failInvocationWant)
}
func setupValkeyDB(t *testing.T, ctx context.Context, client valkey.Client) func(*testing.T) {
keys := []string{"row1", "row2", "row3"}
keys := []string{"row1", "row2", "row3", "row4"}
commands := [][]string{
{"HSET", keys[0], "name", "Alice", "id", "1"},
{"HSET", keys[1], "name", "Jane", "id", "2"},
{"HSET", keys[2], "name", "Sid", "id", "3"},
{"HSET", tests.SERVICE_ACCOUNT_EMAIL, "name", "Alice"},
{"HSET", keys[3], "name", "", "id", "4"},
{"HSET", tests.ServiceAccountEmail, "name", "Alice"},
}
builtCmds := make(valkey.Commands, len(commands))