Skip to content

Commit 1a278bd

Browse files
Quarz0Yuan325
authored andcommitted
feat(sources/healthcare): add a new healthcare source (#1656)
## Description Add a new Healthcare Dataset source ## PR Checklist --- > Thank you for opening a Pull Request! Before submitting your PR, there are a > few things you can do to make sure it goes smoothly: - [x] Make sure you reviewed [CONTRIBUTING.md](https://github.com/googleapis/genai-toolbox/blob/main/CONTRIBUTING.md) - [x] Make sure to open an issue as a [bug/issue](https://github.com/googleapis/genai-toolbox/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [x] Ensure the tests and linter pass - [x] Code coverage does not decrease (if any source code was changed) - [x] Appropriate docs were updated (if necessary) - [x] Make sure to add `!` if this involve a breaking change 🛠️ Fixes #1658 ---------
1 parent ecd533e commit 1a278bd

File tree

3 files changed

+546
-0
lines changed

3 files changed

+546
-0
lines changed
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
---
2+
title: "Cloud Healthcare API"
3+
linkTitle: "Healthcare"
4+
type: docs
5+
weight: 1
6+
description: >
7+
The Cloud Healthcare API provides a managed solution for storing and
8+
accessing healthcare data in Google Cloud, providing a critical bridge
9+
between existing care systems and applications hosted on Google Cloud.
10+
---
11+
12+
## About
13+
14+
The [Cloud Healthcare API][healthcare-docs] provides a managed solution
15+
for storing and accessing healthcare data in Google Cloud, providing a
16+
critical bridge between existing care systems and applications hosted on
17+
Google Cloud. It supports healthcare data standards such as HL7® FHIR®,
18+
HL7® v2, and DICOM®. It provides a fully managed, highly scalable,
19+
enterprise-grade development environment for building clinical and analytics
20+
solutions securely on Google Cloud.
21+
22+
A dataset is a container in your Google Cloud project that holds modality-specific
23+
healthcare data. Datasets contain other data stores, such as FHIR stores and DICOM
24+
stores, which in turn hold their own types of healthcare data.
25+
26+
A single dataset can contain one or many data stores, and those stores can all service
27+
the same modality or different modalities as application needs dictate. Using multiple
28+
stores in the same dataset might be appropriate in various situations.
29+
30+
If you are new to the Healthcare API, you can try to
31+
[create and view datasets and stores using curl][healthcare-quickstart-curl].
32+
33+
[healthcare-docs]: https://cloud.google.com/healthcare/docs
34+
[healthcare-quickstart-curl]:
35+
https://cloud.google.com/healthcare-api/docs/store-healthcare-data-rest
36+
37+
## Requirements
38+
39+
### IAM Permissions
40+
41+
The Healthcare API uses [Identity and Access Management (IAM)][iam-overview] to control
42+
user and group access to Healthcare resources like projects, datasets, and stores.
43+
44+
### Authentication via Application Default Credentials (ADC)
45+
46+
By **default**, Toolbox will use your [Application Default Credentials
47+
(ADC)][adc] to authorize and authenticate when interacting with the
48+
[Healthcare API][healthcare-docs].
49+
50+
When using this method, you need to ensure the IAM identity associated with your
51+
ADC (such as a service account) has the correct permissions for the queries you
52+
intend to run. Common roles include `roles/healthcare.fhirResourceReader` (which includes
53+
permissions to read and search for FHIR resources) or `roles/healthcare.dicomViewer` (for
54+
retrieving DICOM images).
55+
Follow this [guide][set-adc] to set up your ADC.
56+
57+
### Authentication via User's OAuth Access Token
58+
59+
If the `useClientOAuth` parameter is set to `true`, Toolbox will instead use the
60+
OAuth access token for authentication. This token is parsed from the
61+
`Authorization` header passed in with the tool invocation request. This method
62+
allows Toolbox to make queries to the [Healthcare API][healthcare-docs] on behalf of the
63+
client or the end-user.
64+
65+
When using this on-behalf-of authentication, you must ensure that the
66+
identity used has been granted the correct IAM permissions.
67+
68+
[iam-overview]: <https://cloud.google.com/healthcare/docs/access-control>
69+
[adc]: <https://cloud.google.com/docs/authentication#adc>
70+
[set-adc]: <https://cloud.google.com/docs/authentication/provide-credentials-adc>
71+
72+
## Example
73+
74+
Initialize a Healthcare source that uses ADC:
75+
76+
```yaml
77+
sources:
78+
my-healthcare-source:
79+
kind: "healthcare"
80+
project: "my-project-id"
81+
region: "us-central1"
82+
dataset: "my-healthcare-dataset-id"
83+
# allowedFhirStores: # Optional: Restricts tool access to a specific list of FHIR store IDs.
84+
# - "my_fhir_store_1"
85+
# allowedDicomStores: # Optional: Restricts tool access to a specific list of DICOM store IDs.
86+
# - "my_dicom_store_1"
87+
# - "my_dicom_store_2"
88+
```
89+
90+
Initialize a Healthcare source that uses the client's access token:
91+
92+
```yaml
93+
sources:
94+
my-healthcare-client-auth-source:
95+
kind: "healthcare"
96+
project: "my-project-id"
97+
region: "us-central1"
98+
dataset: "my-healthcare-dataset-id"
99+
useClientOAuth: true
100+
# allowedFhirStores: # Optional: Restricts tool access to a specific list of FHIR store IDs.
101+
# - "my_fhir_store_1"
102+
# allowedDicomStores: # Optional: Restricts tool access to a specific list of DICOM store IDs.
103+
# - "my_dicom_store_1"
104+
# - "my_dicom_store_2"
105+
```
106+
107+
## Reference
108+
109+
| **field** | **type** | **required** | **description** |
110+
|--------------------|:--------:|:------------:|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
111+
| kind | string | true | Must be "healthcare". |
112+
| project | string | true | ID of the GCP project that the dataset lives in. |
113+
| region | string | true | Specifies the region (e.g., 'us', 'asia-northeast1') of the healthcare dataset. [Learn More](https://cloud.google.com/healthcare-api/docs/regions) |
114+
| dataset | string | true | ID of the healthcare dataset. |
115+
| allowedFhirStores | []string | false | An optional list of FHIR store IDs that tools using this source are allowed to access. If provided, any tool operation attempting to access a store not in this list will be rejected. If a single store is provided, it will be treated as the default for prebuilt tools. |
116+
| allowedDicomStores | []string | false | An optional list of DICOM store IDs that tools using this source are allowed to access. If provided, any tool operation attempting to access a store not in this list will be rejected. If a single store is provided, it will be treated as the default for prebuilt tools. |
117+
| useClientOAuth | bool | false | If true, forwards the client's OAuth access token from the "Authorization" header to downstream queries. |
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package healthcare
16+
17+
import (
18+
"context"
19+
"fmt"
20+
"net/http"
21+
22+
"github.com/goccy/go-yaml"
23+
"github.com/googleapis/genai-toolbox/internal/sources"
24+
"github.com/googleapis/genai-toolbox/internal/util"
25+
"go.opentelemetry.io/otel/trace"
26+
"golang.org/x/oauth2"
27+
"golang.org/x/oauth2/google"
28+
"google.golang.org/api/googleapi"
29+
"google.golang.org/api/healthcare/v1"
30+
"google.golang.org/api/option"
31+
)
32+
33+
const SourceKind string = "healthcare"
34+
35+
// validate interface
36+
var _ sources.SourceConfig = Config{}
37+
38+
type HealthcareServiceCreator func(tokenString string) (*healthcare.Service, error)
39+
40+
func init() {
41+
if !sources.Register(SourceKind, newConfig) {
42+
panic(fmt.Sprintf("source kind %q already registered", SourceKind))
43+
}
44+
}
45+
46+
func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (sources.SourceConfig, error) {
47+
actual := Config{Name: name}
48+
if err := decoder.DecodeContext(ctx, &actual); err != nil {
49+
return nil, err
50+
}
51+
return actual, nil
52+
}
53+
54+
type Config struct {
55+
// Healthcare configs
56+
Name string `yaml:"name" validate:"required"`
57+
Kind string `yaml:"kind" validate:"required"`
58+
Project string `yaml:"project" validate:"required"`
59+
Region string `yaml:"region" validate:"required"`
60+
Dataset string `yaml:"dataset" validate:"required"`
61+
AllowedFHIRStores []string `yaml:"allowedFhirStores"`
62+
AllowedDICOMStores []string `yaml:"allowedDicomStores"`
63+
UseClientOAuth bool `yaml:"useClientOAuth"`
64+
}
65+
66+
func (c Config) SourceConfigKind() string {
67+
return SourceKind
68+
}
69+
70+
func (c Config) Initialize(ctx context.Context, tracer trace.Tracer) (sources.Source, error) {
71+
var service *healthcare.Service
72+
var serviceCreator HealthcareServiceCreator
73+
var tokenSource oauth2.TokenSource
74+
75+
svc, tok, err := initHealthcareConnection(ctx, tracer, c.Name)
76+
if err != nil {
77+
return nil, fmt.Errorf("error creating service from ADC: %w", err)
78+
}
79+
if c.UseClientOAuth {
80+
serviceCreator, err = newHealthcareServiceCreator(ctx, tracer, c.Name)
81+
if err != nil {
82+
return nil, fmt.Errorf("error constructing service creator: %w", err)
83+
}
84+
} else {
85+
service = svc
86+
tokenSource = tok
87+
}
88+
89+
dsName := fmt.Sprintf("projects/%s/locations/%s/datasets/%s", c.Project, c.Region, c.Dataset)
90+
if _, err = svc.Projects.Locations.Datasets.FhirStores.Get(dsName).Do(); err != nil {
91+
if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == http.StatusNotFound {
92+
return nil, fmt.Errorf("dataset '%s' not found", dsName)
93+
}
94+
return nil, fmt.Errorf("failed to verify existence of dataset '%s': %w", dsName, err)
95+
}
96+
97+
allowedFHIRStores := make(map[string]struct{})
98+
for _, store := range c.AllowedFHIRStores {
99+
name := fmt.Sprintf("%s/fhirStores/%s", dsName, store)
100+
_, err := svc.Projects.Locations.Datasets.FhirStores.Get(name).Do()
101+
if err != nil {
102+
if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == http.StatusNotFound {
103+
return nil, fmt.Errorf("allowedFhirStore '%s' not found in dataset '%s'", store, dsName)
104+
}
105+
return nil, fmt.Errorf("failed to verify allowedFhirStore '%s' in datasest '%s': %w", store, dsName, err)
106+
}
107+
allowedFHIRStores[store] = struct{}{}
108+
}
109+
allowedDICOMStores := make(map[string]struct{})
110+
for _, store := range c.AllowedDICOMStores {
111+
name := fmt.Sprintf("%s/dicomStores/%s", dsName, store)
112+
_, err := svc.Projects.Locations.Datasets.DicomStores.Get(name).Do()
113+
if err != nil {
114+
if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == http.StatusNotFound {
115+
return nil, fmt.Errorf("allowedDicomStore '%s' not found in dataset '%s'", store, dsName)
116+
}
117+
return nil, fmt.Errorf("failed to verify allowedDicomFhirStore '%s' in datasest '%s': %w", store, dsName, err)
118+
}
119+
allowedDICOMStores[store] = struct{}{}
120+
}
121+
s := &Source{
122+
name: c.Name,
123+
kind: SourceKind,
124+
project: c.Project,
125+
region: c.Region,
126+
dataset: c.Dataset,
127+
service: service,
128+
serviceCreator: serviceCreator,
129+
tokenSource: tokenSource,
130+
allowedFHIRStores: allowedFHIRStores,
131+
allowedDICOMStores: allowedDICOMStores,
132+
useClientOAuth: c.UseClientOAuth,
133+
}
134+
return s, nil
135+
}
136+
137+
func newHealthcareServiceCreator(ctx context.Context, tracer trace.Tracer, name string) (func(string) (*healthcare.Service, error), error) {
138+
userAgent, err := util.UserAgentFromContext(ctx)
139+
if err != nil {
140+
return nil, err
141+
}
142+
return func(tokenString string) (*healthcare.Service, error) {
143+
return initHealthcareConnectionWithOAuthToken(ctx, tracer, name, userAgent, tokenString)
144+
}, nil
145+
}
146+
147+
func initHealthcareConnectionWithOAuthToken(ctx context.Context, tracer trace.Tracer, name string, userAgent string, tokenString string) (*healthcare.Service, error) {
148+
ctx, span := sources.InitConnectionSpan(ctx, tracer, SourceKind, name)
149+
defer span.End()
150+
// Construct token source
151+
token := &oauth2.Token{
152+
AccessToken: string(tokenString),
153+
}
154+
ts := oauth2.StaticTokenSource(token)
155+
156+
// Initialize the Healthcare service with tokenSource
157+
service, err := healthcare.NewService(ctx, option.WithUserAgent(userAgent), option.WithTokenSource(ts))
158+
if err != nil {
159+
return nil, fmt.Errorf("failed to create Healthcare service: %w", err)
160+
}
161+
return service, nil
162+
}
163+
164+
func initHealthcareConnection(ctx context.Context, tracer trace.Tracer, name string) (*healthcare.Service, oauth2.TokenSource, error) {
165+
ctx, span := sources.InitConnectionSpan(ctx, tracer, SourceKind, name)
166+
defer span.End()
167+
168+
cred, err := google.FindDefaultCredentials(ctx, healthcare.CloudHealthcareScope)
169+
if err != nil {
170+
return nil, nil, fmt.Errorf("failed to find default Google Cloud credentials with scope %q: %w", healthcare.CloudHealthcareScope, err)
171+
}
172+
173+
userAgent, err := util.UserAgentFromContext(ctx)
174+
if err != nil {
175+
return nil, nil, err
176+
}
177+
178+
service, err := healthcare.NewService(ctx, option.WithUserAgent(userAgent), option.WithCredentials(cred))
179+
if err != nil {
180+
return nil, nil, fmt.Errorf("failed to create Healthcare service: %w", err)
181+
}
182+
return service, cred.TokenSource, nil
183+
}
184+
185+
var _ sources.Source = &Source{}
186+
187+
type Source struct {
188+
name string `yaml:"name"`
189+
kind string `yaml:"kind"`
190+
project string
191+
region string
192+
dataset string
193+
service *healthcare.Service
194+
serviceCreator HealthcareServiceCreator
195+
tokenSource oauth2.TokenSource
196+
allowedFHIRStores map[string]struct{}
197+
allowedDICOMStores map[string]struct{}
198+
useClientOAuth bool
199+
}
200+
201+
func (s *Source) SourceKind() string {
202+
return SourceKind
203+
}
204+
205+
func (s *Source) Project() string {
206+
return s.project
207+
}
208+
209+
func (s *Source) Region() string {
210+
return s.region
211+
}
212+
213+
func (s *Source) DatasetID() string {
214+
return s.dataset
215+
}
216+
217+
func (s *Source) Service() *healthcare.Service {
218+
return s.service
219+
}
220+
221+
func (s *Source) ServiceCreator() HealthcareServiceCreator {
222+
return s.serviceCreator
223+
}
224+
225+
func (s *Source) TokenSource() oauth2.TokenSource {
226+
return s.tokenSource
227+
}
228+
229+
func (s *Source) AllowedFHIRStores() map[string]struct{} {
230+
if len(s.allowedFHIRStores) == 0 {
231+
return nil
232+
}
233+
return s.allowedFHIRStores
234+
}
235+
236+
func (s *Source) AllowedDICOMStores() map[string]struct{} {
237+
if len(s.allowedDICOMStores) == 0 {
238+
return nil
239+
}
240+
return s.allowedDICOMStores
241+
}
242+
243+
func (s *Source) IsFHIRStoreAllowed(storeID string) bool {
244+
if len(s.allowedFHIRStores) == 0 {
245+
return true
246+
}
247+
_, ok := s.allowedFHIRStores[storeID]
248+
return ok
249+
}
250+
251+
func (s *Source) IsDICOMStoreAllowed(storeID string) bool {
252+
if len(s.allowedDICOMStores) == 0 {
253+
return true
254+
}
255+
_, ok := s.allowedDICOMStores[storeID]
256+
return ok
257+
}
258+
259+
func (s *Source) UseClientAuthorization() bool {
260+
return s.useClientOAuth
261+
}

0 commit comments

Comments
 (0)