blob: 9f51aaf164650cbd4d2f6ec223c63ea3c039ac0d [file]
// Copyright 2026 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package embeddings
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"time"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
)
const (
// cloudPlatformScope is the OAuth2 scope required to call Google Cloud APIs.
cloudPlatformScope = "https://www.googleapis.com/auth/cloud-platform"
// outputDimensionality is the number of dimensions for output embeddings.
// This must match the DB schema: embedding halfvec(256).
outputDimensionality = 256
// defaultTimeout is the HTTP request timeout for Vertex AI prediction.
defaultTimeout = 10 * time.Second
// defaultModel is the embedding model name.
// WARNING: Changing this requires re-embedding all documents in the database.
defaultModel = "text-embedding-004"
)
// TaskType specifies the Vertex AI embedding task type.
// See https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/embeddings/task-types
type TaskType string
const (
// TaskTypeDocument is used for indexing static document content.
TaskTypeDocument TaskType = "RETRIEVAL_DOCUMENT"
// TaskTypeQuery is used for user search queries.
TaskTypeQuery TaskType = "RETRIEVAL_QUERY"
)
// Client is a client for the Vertex AI Embeddings API.
type Client struct {
HTTPClient *http.Client
BaseURL string
projectID string
location string
model string
}
// NewClient creates a new Client.
// location is the GCP region (e.g., "us-central1").
func NewClient(ctx context.Context, location string) (*Client, error) {
creds, err := google.FindDefaultCredentials(ctx, cloudPlatformScope)
if err != nil {
return nil, fmt.Errorf("google.FindDefaultCredentials: %w", err)
}
projectID := creds.ProjectID
if projectID == "" {
projectID = os.Getenv("GOOGLE_CLOUD_PROJECT")
}
if projectID == "" {
return nil, fmt.Errorf("GCP project ID is empty; set GOOGLE_CLOUD_PROJECT environment variable")
}
httpClient := oauth2.NewClient(ctx, creds.TokenSource)
httpClient.Timeout = defaultTimeout
return &Client{
HTTPClient: httpClient,
projectID: projectID,
location: location,
model: defaultModel,
}, nil
}
type predictRequest struct {
Instances []instance `json:"instances"`
Parameters struct {
OutputDimensionality int `json:"outputDimensionality,omitempty"`
} `json:"parameters,omitempty"`
}
type instance struct {
Content string `json:"content"`
TaskType TaskType `json:"taskType,omitempty"`
}
type predictResponse struct {
Predictions []struct {
Embeddings struct {
Values []float32 `json:"values"`
} `json:"embeddings"`
} `json:"predictions"`
}
// GenerateEmbeddings gets 256-dimensional embeddings for the given texts using the Vertex AI API.
func (c *Client) GenerateEmbeddings(ctx context.Context, texts []string, taskType TaskType) ([][]float32, error) {
if len(texts) == 0 {
return nil, nil
}
var reqBody predictRequest
reqBody.Parameters.OutputDimensionality = outputDimensionality
for _, text := range texts {
reqBody.Instances = append(reqBody.Instances, instance{
Content: text,
TaskType: taskType,
})
}
jsonBytes, err := json.Marshal(reqBody)
if err != nil {
return nil, err
}
url := c.BaseURL
if url == "" {
url = fmt.Sprintf("https://%s-aiplatform.googleapis.com/v1/projects/%s/locations/%s/publishers/google/models/%s:predict",
c.location, c.projectID, c.location, c.model)
}
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(jsonBytes))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
hc := c.HTTPClient
if hc == nil {
hc = http.DefaultClient
}
resp, err := hc.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
bodyBytes, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("vertex AI predict failed (status %d): %s", resp.StatusCode, string(bodyBytes))
}
var respBody predictResponse
if err := json.NewDecoder(resp.Body).Decode(&respBody); err != nil {
return nil, err
}
if len(respBody.Predictions) != len(texts) {
return nil, fmt.Errorf("expected %d predictions, got %d", len(texts), len(respBody.Predictions))
}
results := make([][]float32, len(texts))
for i, pred := range respBody.Predictions {
results[i] = pred.Embeddings.Values
}
return results, nil
}