Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
46 changes: 32 additions & 14 deletions .ci/integration.cloudbuild.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
steps:
- id: "detect-changes"
name: "gcr.io/cloud-builders/git"
waitFor: ["-"]
waitFor: ["-"]
entrypoint: "bash"
args:
- -c
Expand Down Expand Up @@ -98,15 +98,20 @@ steps:
- "CLOUD_SQL_POSTGRES_REGION=$_REGION"
- "SERVICE_ACCOUNT_EMAIL=$SERVICE_ACCOUNT_EMAIL"
secretEnv:
["CLOUD_SQL_POSTGRES_USER", "CLOUD_SQL_POSTGRES_PASS", "CLIENT_ID", "API_KEY"]
[
"CLOUD_SQL_POSTGRES_USER",
"CLOUD_SQL_POSTGRES_PASS",
"CLIENT_ID",
"API_KEY",
]
volumes:
- name: "go"
path: "/gopath"
args:
- -c
- |
PATTERN="cloudsqlpg|internal/sources/postgres|tests/postgres|internal/server/|tests/common.go|tests/tool.go|.ci/"

if grep -qE "$$PATTERN" /workspace/changed_files.txt; then
echo "Relevant changes detected. Starting Cloud SQL Postgres tests..."
.ci/test_with_coverage.sh \
Expand Down Expand Up @@ -137,7 +142,7 @@ steps:
- -c
- |
PATTERN="alloydb|internal/sources/postgres|tests/postgres|internal/server/|tests/common.go|.ci/"

if grep -qE "$$PATTERN" /workspace/changed_files.txt; then
echo "Relevant changes detected. Starting AlloyDB tests..."
.ci/test_with_coverage.sh \
Expand All @@ -161,7 +166,13 @@ steps:
- "ALLOYDB_POSTGRES_DATABASE=$_DATABASE_NAME"
- "ALLOYDB_POSTGRES_REGION=$_REGION"
- "SERVICE_ACCOUNT_EMAIL=$SERVICE_ACCOUNT_EMAIL"
secretEnv: ["ALLOYDB_POSTGRES_USER", "ALLOYDB_POSTGRES_PASSWORD", "CLIENT_ID", "API_KEY"]
secretEnv:
[
"ALLOYDB_POSTGRES_USER",
"ALLOYDB_POSTGRES_PASSWORD",
"CLIENT_ID",
"API_KEY",
]
volumes:
- name: "go"
path: "/gopath"
Expand Down Expand Up @@ -508,7 +519,7 @@ steps:
- "SPANNER_DATABASE=$_DATABASE_NAME"
- "SPANNER_INSTANCE=$_SPANNER_INSTANCE"
- "SERVICE_ACCOUNT_EMAIL=$SERVICE_ACCOUNT_EMAIL"
secretEnv: ["CLIENT_ID"]
secretEnv: ["CLIENT_ID", "API_KEY"]
volumes:
- name: "go"
path: "/gopath"
Expand Down Expand Up @@ -757,8 +768,7 @@ steps:
env:
- "GOPATH=/gopath"
- "SERVICE_ACCOUNT_EMAIL=$SERVICE_ACCOUNT_EMAIL"
secretEnv:
["CLIENT_ID"]
secretEnv: ["CLIENT_ID"]
volumes:
- name: "go"
path: "/gopath"
Expand Down Expand Up @@ -1159,7 +1169,13 @@ steps:
env:
- "GOPATH=/gopath"
- "SERVICE_ACCOUNT_EMAIL=$SERVICE_ACCOUNT_EMAIL"
secretEnv: ["CLIENT_ID", "ELASTICSEARCH_USER", "ELASTICSEARCH_PASS", "ELASTICSEARCH_HOST"]
secretEnv:
[
"CLIENT_ID",
"ELASTICSEARCH_USER",
"ELASTICSEARCH_PASS",
"ELASTICSEARCH_HOST",
]
volumes:
- name: "go"
path: "/gopath"
Expand Down Expand Up @@ -1188,7 +1204,8 @@ steps:
- "SERVICE_ACCOUNT_EMAIL=$SERVICE_ACCOUNT_EMAIL"
- "SNOWFLAKE_DATABASE=$_SNOWFLAKE_DATABASE"
- "SNOWFLAKE_SCHEMA=$_SNOWFLAKE_SCHEMA"
secretEnv: ["CLIENT_ID", "SNOWFLAKE_USER", "SNOWFLAKE_PASS", "SNOWFLAKE_ACCOUNT"]
secretEnv:
["CLIENT_ID", "SNOWFLAKE_USER", "SNOWFLAKE_PASS", "SNOWFLAKE_ACCOUNT"]
volumes:
- name: "go"
path: "/gopath"
Expand All @@ -1215,7 +1232,8 @@ steps:
env:
- "GOPATH=/gopath"
- "SERVICE_ACCOUNT_EMAIL=$SERVICE_ACCOUNT_EMAIL"
secretEnv: ["CLIENT_ID", "CASSANDRA_USER", "CASSANDRA_PASS", "CASSANDRA_HOST"]
secretEnv:
["CLIENT_ID", "CASSANDRA_USER", "CASSANDRA_PASS", "CASSANDRA_HOST"]
volumes:
- name: "go"
path: "/gopath"
Expand All @@ -1237,13 +1255,14 @@ steps:

- id: "oracle"
name: ghcr.io/oracle/oraclelinux9-instantclient:23
waitFor: ["install-dependencies","detect-changes"]
waitFor: ["install-dependencies", "detect-changes"]
entrypoint: /bin/bash
env:
- "GOPATH=/gopath"
- "SERVICE_ACCOUNT_EMAIL=$SERVICE_ACCOUNT_EMAIL"
- "ORACLE_SERVER_NAME=$_ORACLE_SERVER_NAME"
secretEnv: ["CLIENT_ID", "ORACLE_USERNAME", "ORACLE_PASSWORD", "ORACLE_HOST"]
secretEnv:
["CLIENT_ID", "ORACLE_USERNAME", "ORACLE_PASSWORD", "ORACLE_HOST"]
volumes:
- name: "go"
path: "/gopath"
Expand Down Expand Up @@ -1384,7 +1403,6 @@ steps:
exit 0
fi


availableSecrets:
secretManager:
# Common secrets
Expand Down
70 changes: 70 additions & 0 deletions docs/en/integrations/spanner/tools/spanner-sql.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,76 @@ parameters:
{{< /tab >}}
{{< /tabpane >}}


### Example with Vector Search

Spanner supports high-performance vector similarity search. When using an `embeddingModel` with a `spanner-sql` tool, the tool automatically converts text parameters into the native ARRAY<FLOAT64> format required by Spanner.

#### Define the Embedding Model
See [EmbeddingModels](../../../documentation/configuration/embedding-models/_index.md) for more information.

```yaml
kind: embeddingModel
name: gemini-model
type: gemini
model: gemini-embedding-001
apiKey: ${GOOGLE_API_KEY}
dimension: 768
```

#### Vector Ingestion Tool
This tool stores both the raw text and its vector representation. It uses `valueFromParam` to hide the vector conversion logic from the LLM, ensuring the Agent only has to provide the content once.

```yaml
kind: tool
name: insert_doc_spanner
type: spanner-sql
source: my-spanner-source
statement: |
INSERT INTO vector_table (id, content, embedding)
VALUES (1, @content, @text_to_embed)
description: |
Index new documents for semantic search in Spanner.
parameters:
- name: content
type: string
description: The text content to store.
- name: text_to_embed
type: string
# Automatically copies 'content' and converts it to a FLOAT64 array
valueFromParam: content
embeddedBy: gemini-model
```

#### Vector Search Tool
This tool allows the Agent to perform a natural language search. The query string provided by the Agent is converted into a vector before the SQL is executed.

```yaml
kind: tool
name: search_docs_spanner
type: spanner-sql
source: my-spanner-source
statement: |
SELECT
id,
content,
COSINE_DISTANCE(embedding, @query) AS distance
FROM
vector_table
ORDER BY
distance
LIMIT 1
description: |
Search for documents in Spanner using natural language.
Returns the most semantically similar result.
parameters:
- name: query
type: string
description: The search query to be converted to a vector.
embeddedBy: gemini-model
```


### Example with Template Parameters

> **Note:** This tool allows direct modifications to the SQL statement,
Expand Down
2 changes: 1 addition & 1 deletion internal/embeddingmodels/gemini/gemini.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func (cfg Config) Initialize(ctx context.Context) (embeddingmodels.EmbeddingMode
// Create new Gemini API client
client, err := genai.NewClient(ctx, configs)
if err != nil {
return nil, fmt.Errorf("unable to create Gemini API client")
return nil, fmt.Errorf("unable to create Gemini API client: %w", err)
}

m := &EmbeddingModel{
Expand Down
53 changes: 53 additions & 0 deletions tests/spanner/spanner_integration_test.go
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we test against both googlesql and postgresql sources?

Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,14 @@ func TestSpannerToolEndpoints(t *testing.T) {
toolsFile = addSpannerListTablesConfig(t, toolsFile)
toolsFile = addSpannerListGraphsConfig(t, toolsFile)

// Set up table for semantic search
vectorTableName, tearDownVectorTable := setupSpannerVectorTable(t, ctx, adminClient, dbString)
defer tearDownVectorTable(t)

// Add semantic search tool config
insertStmt, searchStmt := getSpannerVectorSearchStmts(vectorTableName)
toolsFile = tests.AddSemanticSearchConfig(t, toolsFile, SpannerToolType, insertStmt, searchStmt)

cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...)
if err != nil {
t.Fatalf("command initialization returned an error: %s", err)
Expand Down Expand Up @@ -205,6 +213,7 @@ func TestSpannerToolEndpoints(t *testing.T) {
runSpannerExecuteSqlToolInvokeTest(t, select1Want, invokeParamWant, tableNameParam)
runSpannerListTablesTest(t, tableNameParam, tableNameAuth, tableNameTemplateParam)
runSpannerListGraphsTest(t, graphName)
tests.RunSemanticSearchToolInvokeTest(t, "null", "", "The quick brown fox")
}

// getSpannerToolInfo returns statements and param for my-tool for spanner-sql type
Expand Down Expand Up @@ -840,6 +849,50 @@ func runSpannerListGraphsTest(t *testing.T, graphName string) {
}
}

// setupSpannerVectorTable creates a vector table in Spanner for semantic search testing
func setupSpannerVectorTable(t *testing.T, ctx context.Context, adminClient *database.DatabaseAdminClient, dbString string) (string, func(*testing.T)) {
tableName := "vector_table_" + strings.ReplaceAll(uuid.New().String(), "-", "")
createStatement := fmt.Sprintf(`CREATE TABLE %s (
id INT64,
content STRING(MAX),
embedding ARRAY<FLOAT32>
) PRIMARY KEY (id)`, tableName)

op, err := adminClient.UpdateDatabaseDdl(ctx, &databasepb.UpdateDatabaseDdlRequest{
Database: dbString,
Statements: []string{createStatement},
})
if err != nil {
t.Fatalf("unable to start create vector table operation %s: %s", tableName, err)
}
err = op.Wait(ctx)
if err != nil {
t.Fatalf("unable to create test vector table %s: %s", tableName, err)
}

return tableName, func(t *testing.T) {
op, err = adminClient.UpdateDatabaseDdl(ctx, &databasepb.UpdateDatabaseDdlRequest{
Database: dbString,
Statements: []string{fmt.Sprintf("DROP TABLE IF EXISTS %s", tableName)},
})
if err != nil {
t.Errorf("unable to start drop %s operation: %s", tableName, err)
return
}
opErr := op.Wait(ctx)
if opErr != nil {
t.Errorf("Teardown failed: %s", opErr)
}
}
}

// getSpannerVectorSearchStmts returns statements for spanner semantic search
func getSpannerVectorSearchStmts(vectorTableName string) (string, string) {
insertStmt := fmt.Sprintf("INSERT INTO %s (id, content, embedding) VALUES (1, @content, @text_to_embed)", vectorTableName)
searchStmt := fmt.Sprintf("SELECT id, content, COSINE_DISTANCE(embedding, @query) AS distance FROM %s ORDER BY distance LIMIT 1", vectorTableName)
return insertStmt, searchStmt
}

func runSpannerSchemaToolInvokeTest(t *testing.T, accessSchemaWant string) {
invokeTcs := []struct {
name string
Expand Down
Loading