Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
758817d
feat: add DataScan API to dataplex source
Andres-Ayala1 Feb 6, 2026
ae34cb6
feat:add search-dq-scans tool to prebuilt configs
Andres-Ayala1 Feb 6, 2026
6f78e51
test: test for dataplex search dq scans tool
Andres-Ayala1 Feb 6, 2026
d33cc22
feat: dataplexsearchdqscans tool
Andres-Ayala1 Feb 6, 2026
4429ed9
docs: dataplex-search-dq-scans docs
Andres-Ayala1 Feb 6, 2026
37e2c3e
test: add new tool searchdqscans to TestPrebuiltTools
Andres-Ayala1 Feb 10, 2026
2ebf9d0
test: add search dq scans invoke and get integration tests
Andres-Ayala1 Feb 10, 2026
febac4c
chore: remove comments
Andres-Ayala1 Feb 17, 2026
1416018
fix: replace parameter for Search DataQualityScans from query to filt…
Andres-Ayala1 Feb 17, 2026
4821398
Merge branch 'main' into feat/dataplex-search-dq-scans
Andres-Ayala1 Feb 17, 2026
72fd260
feat: change prebuilt tools check to new cmd/internal/skills tools_fi…
Andres-Ayala1 Feb 17, 2026
9b7d961
docs: parameters for search-dq-scans tool data scan id and table name
Andres-Ayala1 Feb 17, 2026
6dd9ee5
fix: util errors for compilation to pass
Andres-Ayala1 Feb 17, 2026
1f9713f
chore: license year headers
Andres-Ayala1 Feb 17, 2026
b899ae2
fix: registration of searchdqscans tool
Andres-Ayala1 Feb 17, 2026
074f968
Merge branch 'main' into feat/dataplex-search-dq-scans
duwenxin99 Feb 19, 2026
462a376
chore: lint formatting
Andres-Ayala1 Feb 20, 2026
1d527af
fix: correct table filter for data quality scans
Andres-Ayala1 Feb 20, 2026
671480d
fix: passing full resource id of table and dataset properly to integr…
Andres-Ayala1 Feb 20, 2026
310eb1b
Merge branch 'main' into feat/dataplex-search-dq-scans
duwenxin99 Mar 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/internal/imports.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ import (
_ "github.com/googleapis/genai-toolbox/internal/tools/dataform/dataformcompilelocal"
_ "github.com/googleapis/genai-toolbox/internal/tools/dataplex/dataplexlookupentry"
_ "github.com/googleapis/genai-toolbox/internal/tools/dataplex/dataplexsearchaspecttypes"
_ "github.com/googleapis/genai-toolbox/internal/tools/dataplex/dataplexsearchdqscans"
_ "github.com/googleapis/genai-toolbox/internal/tools/dataplex/dataplexsearchentries"
_ "github.com/googleapis/genai-toolbox/internal/tools/dataproc/dataprocgetcluster"
_ "github.com/googleapis/genai-toolbox/internal/tools/dataproc/dataprocgetjob"
Expand Down
2 changes: 1 addition & 1 deletion cmd/internal/tools_file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1733,7 +1733,7 @@ func TestPrebuiltTools(t *testing.T) {
wantToolset: server.ToolsetConfigs{
"dataplex_tools": tools.ToolsetConfig{
Name: "dataplex_tools",
ToolNames: []string{"search_entries", "lookup_entry", "search_aspect_types"},
ToolNames: []string{"search_entries", "lookup_entry", "search_aspect_types", "search_dq_scans"},
},
},
},
Expand Down
64 changes: 64 additions & 0 deletions docs/en/resources/tools/dataplex/dataplex-search-dq-scans.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
---
title: "dataplex-search-dq-scans"
type: docs
weight: 1
description: >
A "dataplex-search-dq-scans" tool allows to search for data quality scans based on the provided parameters.
aliases:
- /resources/tools/dataplex-search-dq-scans
---

## About

A `dataplex-search-dq-scans` tool returns data quality scans that match the given criteria.
It's compatible with the following sources:

- [dataplex](../../sources/dataplex.md)

`dataplex-search-dq-scans` accepts the following optional parameters:

- `filter` - Filter string to search/filter data quality scans. E.g. "display_name = \"my-scan\"".
- `data_scan_id` - The resource name of the data scan to filter by: projects/{project}/locations/{locationId}/dataScans/{dataScanId}.
- `table_name` - The name of the table to filter by. Maps to data.entity in the filter string. E.g. "//bigquery.googleapis.com/projects/P/datasets/D/tables/T".
- `pageSize` - Number of returned data quality scans in the page. Defaults to `10`.
- `orderBy` - Specifies the ordering of results.

## Requirements

### IAM Permissions

Dataplex uses [Identity and Access Management (IAM)][iam-overview] to control
user and group access to Dataplex resources. Toolbox will use your
[Application Default Credentials (ADC)][adc] to authorize and authenticate when
interacting with [Dataplex][dataplex-docs].

In addition to [setting the ADC for your server][set-adc], you need to ensure
the IAM identity has been given the correct IAM permissions for the tasks you
intend to perform. See [Dataplex Universal Catalog IAM permissions][iam-permissions]
and [Dataplex Universal Catalog IAM roles][iam-roles] for more information on
applying IAM permissions and roles to an identity.

[iam-overview]: https://cloud.google.com/dataplex/docs/iam-and-access-control
[adc]: https://cloud.google.com/docs/authentication#adc
[set-adc]: https://cloud.google.com/docs/authentication/provide-credentials-adc
[iam-permissions]: https://cloud.google.com/dataplex/docs/iam-permissions
[iam-roles]: https://cloud.google.com/dataplex/docs/iam-roles
[dataplex-docs]: https://cloud.google.com/dataplex

## Example

```yaml
kind: tools
name: dataplex-search-dq-scans
type: dataplex-search-dq-scans
source: my-dataplex-source
description: Use this tool to search for data quality scans.
```

## Reference

| **field** | **type** | **required** | **description** |
|-------------|:--------:|:------------:|----------------------------------------------------|
| type | string | true | Must be "dataplex-search-dq-scans". |
| source | string | true | Name of the source the tool should execute on. |
| description | string | true | Description of the tool that is passed to the LLM. |
5 changes: 5 additions & 0 deletions internal/prebuiltconfigs/tools/dataplex.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,14 @@ tools:
kind: dataplex-search-aspect-types
source: dataplex-source
description: Use this tool to find aspect types relevant to the query.
search_dq_scans:
kind: dataplex-search-dq-scans
source: dataplex-source
description: Use this tool to search for data quality scans in Dataplex.

toolsets:
dataplex_tools:
- search_entries
- lookup_entry
- search_aspect_types
- search_dq_scans
59 changes: 50 additions & 9 deletions internal/sources/dataplex/dataplex.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,14 @@ func (r Config) SourceConfigType() string {

func (r Config) Initialize(ctx context.Context, tracer trace.Tracer) (sources.Source, error) {
// Initializes a Dataplex source
client, err := initDataplexConnection(ctx, tracer, r.Name, r.Project)
client, dataScanClient, err := initDataplexConnection(ctx, tracer, r.Name, r.Project)
if err != nil {
return nil, err
}
s := &Source{
Config: r,
Client: client,
Config: r,
Client: client,
DataScanClient: dataScanClient,
}

return s, nil
Expand All @@ -80,7 +81,8 @@ var _ sources.Source = &Source{}

type Source struct {
Config
Client *dataplexapi.CatalogClient
Client *dataplexapi.CatalogClient
DataScanClient *dataplexapi.DataScanClient
}

func (s *Source) SourceType() string {
Expand All @@ -100,30 +102,39 @@ func (s *Source) CatalogClient() *dataplexapi.CatalogClient {
return s.Client
}

func (s *Source) GetDataScanClient() *dataplexapi.DataScanClient {
return s.DataScanClient
}

func initDataplexConnection(
ctx context.Context,
tracer trace.Tracer,
name string,
project string,
) (*dataplexapi.CatalogClient, error) {
) (*dataplexapi.CatalogClient, *dataplexapi.DataScanClient, error) {
ctx, span := sources.InitConnectionSpan(ctx, tracer, SourceType, name)
defer span.End()

cred, err := google.FindDefaultCredentials(ctx)
if err != nil {
return nil, fmt.Errorf("failed to find default Google Cloud credentials: %w", err)
return nil, nil, fmt.Errorf("failed to find default Google Cloud credentials for project %q: %w", project, err)
}

userAgent, err := util.UserAgentFromContext(ctx)
if err != nil {
return nil, err
return nil, nil, err
}

client, err := dataplexapi.NewCatalogClient(ctx, option.WithUserAgent(userAgent), option.WithCredentials(cred))
if err != nil {
return nil, fmt.Errorf("failed to create Dataplex client for project %q: %w", project, err)
return nil, nil, fmt.Errorf("failed to create Dataplex client for project %q: %w", project, err)
}

dataScanClient, err := dataplexapi.NewDataScanClient(ctx, option.WithUserAgent(userAgent), option.WithCredentials(cred))
if err != nil {
return nil, nil, fmt.Errorf("failed to create Dataplex DataScan client for project %q: %w", project, err)
}
return client, nil
return client, dataScanClient, nil
}

func (s *Source) LookupEntry(ctx context.Context, name string, view int, aspectTypes []string, entry string) (*dataplexpb.Entry, error) {
Expand Down Expand Up @@ -240,3 +251,33 @@ func (s *Source) SearchEntries(ctx context.Context, query string, pageSize int,
}
return results, nil
}

func (s *Source) SearchDataQualityScans(ctx context.Context, filter string, pageSize int, orderBy string) ([]*dataplexpb.DataScan, error) {
// ListDataScansRequest doesn't support generic specific "query" field like SearchEntries,
// but it supports "filter". We will assume `query` is passed as filter.
// If the user wants to search by name/display_name they should provide a filter string.
// Example filter: "display_name = \"my-scan\""
req := &dataplexpb.ListDataScansRequest{
Parent: fmt.Sprintf("projects/%s/locations/-", s.ProjectID()),
Filter: filter,
PageSize: int32(pageSize),
OrderBy: orderBy,
}

it := s.GetDataScanClient().ListDataScans(ctx, req)
var results []*dataplexpb.DataScan
for {
scan, err := it.Next()
if err == iterator.Done {
break
}
if err != nil {
if st, ok := grpcstatus.FromError(err); ok {
return nil, fmt.Errorf("failed to list data scans: code=%s message=%s", st.Code(), st.Message())
}
return nil, fmt.Errorf("failed to list data scans: %w", err)
}
results = append(results, scan)
}
return results, nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// Copyright 2026 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.

package dataplexsearchdqscans

import (
"context"
"fmt"
"net/http"
"strings"

"cloud.google.com/go/dataplex/apiv1/dataplexpb"
"github.com/goccy/go-yaml"
"github.com/googleapis/genai-toolbox/internal/embeddingmodels"
"github.com/googleapis/genai-toolbox/internal/sources"
"github.com/googleapis/genai-toolbox/internal/tools"
"github.com/googleapis/genai-toolbox/internal/util"
"github.com/googleapis/genai-toolbox/internal/util/parameters"
)

const resourceType string = "dataplex-search-dq-scans"

func init() {
if !tools.Register(resourceType, newConfig) {
panic(fmt.Sprintf("tool type %q already registered", resourceType))
}
}

func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.ToolConfig, error) {
actual := Config{Name: name}
if err := decoder.DecodeContext(ctx, &actual); err != nil {
return nil, err
}
return actual, nil
}

type compatibleSource interface {
SearchDataQualityScans(context.Context, string, int, string) ([]*dataplexpb.DataScan, error)
}

type Config struct {
Name string `yaml:"name" validate:"required"`
Type string `yaml:"type" validate:"required"`
Source string `yaml:"source" validate:"required"`
Description string `yaml:"description"`
AuthRequired []string `yaml:"authRequired"`
}

// validate interface
var _ tools.ToolConfig = Config{}

func (cfg Config) ToolConfigType() string {
return resourceType
}

func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) {
filter := parameters.NewStringParameterWithDefault("filter", "", "Optional. Filter string to search/filter data quality scans. E.g. \"display_name = \\\"my-scan\\\"\"")
dataScanID := parameters.NewStringParameterWithDefault("data_scan_id", "", "Optional. The resource name of the data scan to filter by: projects/{project}/locations/{locationId}/dataScans/{dataScanId}.")
tableName := parameters.NewStringParameterWithDefault("table_name", "", "Optional. The name of the table to filter by. Maps to data.entity in the filter string. E.g. \"//bigquery.googleapis.com/projects/P/datasets/D/tables/T\"")
pageSize := parameters.NewIntParameterWithDefault("pageSize", 10, "Number of returned data quality scans in the page.")
orderBy := parameters.NewStringParameterWithDefault("orderBy", "", "Specifies the ordering of results.")
params := parameters.Parameters{filter, dataScanID, tableName, pageSize, orderBy}

mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, params, nil)

t := Tool{
Config: cfg,
Parameters: params,
manifest: tools.Manifest{
Description: cfg.Description,
Parameters: params.Manifest(),
AuthRequired: cfg.AuthRequired,
},
mcpManifest: mcpManifest,
}
return t, nil
}

type Tool struct {
Config
Parameters parameters.Parameters
manifest tools.Manifest
mcpManifest tools.McpManifest
}

func (t Tool) ToConfig() tools.ToolConfig {
return t.Config
}

func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, params parameters.ParamValues, accessToken tools.AccessToken) (any, util.ToolboxError) {
source, err := tools.GetCompatibleSource[compatibleSource](resourceMgr, t.Source, t.Name, t.Type)
if err != nil {
return nil, util.NewClientServerError("source used is not compatible with the tool", http.StatusInternalServerError, err)
}
paramsMap := params.AsMap()
filter, _ := paramsMap["filter"].(string)
dataScanID, _ := paramsMap["data_scan_id"].(string)
tableName, _ := paramsMap["table_name"].(string)
pageSize, _ := paramsMap["pageSize"].(int)
orderBy, _ := paramsMap["orderBy"].(string)

var filters []string
if filter != "" {
filters = append(filters, filter)
}
if dataScanID != "" {
filters = append(filters, fmt.Sprintf("name = %q", dataScanID))
}
if tableName != "" {
filters = append(filters, fmt.Sprintf("data.resource = %q", tableName))
}

finalFilter := strings.Join(filters, " AND ")

res, err := source.SearchDataQualityScans(ctx, finalFilter, pageSize, orderBy)
if err != nil {
return nil, util.NewClientServerError("failed to search for dq scans", http.StatusInternalServerError, err)
}
return res, nil
}

func (t Tool) EmbedParams(ctx context.Context, paramValues parameters.ParamValues, embeddingModelsMap map[string]embeddingmodels.EmbeddingModel) (parameters.ParamValues, error) {
return parameters.EmbedParams(ctx, t.Parameters, paramValues, embeddingModelsMap, nil)
}

func (t Tool) Manifest() tools.Manifest {
// Returns the tool manifest
return t.manifest
}

func (t Tool) McpManifest() tools.McpManifest {
// Returns the tool MCP manifest
return t.mcpManifest
}

func (t Tool) Authorized(verifiedAuthServices []string) bool {
return tools.IsAuthorized(t.AuthRequired, verifiedAuthServices)
}

func (t Tool) RequiresClientAuthorization(resourceMgr tools.SourceProvider) (bool, error) {
return false, nil
}

func (t Tool) GetAuthTokenHeaderName(resourceMgr tools.SourceProvider) (string, error) {
return "Authorization", nil
}

func (t Tool) GetParameters() parameters.Parameters {
return t.Parameters
}
Loading
Loading