Skip to content

Commit 989d064

Browse files
MarkDaoustcopybara-github
authored andcommitted
feat: Multimodal file search
Add embeddingModel for create file searech store Add mediaID to GroundingChunkRetrievedContext Add file_search_stores.downloadMedia PiperOrigin-RevId: 910272620
1 parent c32ae6e commit 989d064

3 files changed

Lines changed: 182 additions & 5 deletions

File tree

filesearchstores.go

Lines changed: 49 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

multistep_test.go

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@ package genai
1717
import (
1818
"context"
1919
"encoding/json"
20+
"fmt"
2021
"os"
2122
"path/filepath"
2223
"reflect"
24+
"strings"
2325
"time"
2426
)
2527

@@ -33,6 +35,7 @@ var customTestMethods = map[string]func(ctx context.Context, client *Client, ite
3335
"shared/files/upload_get_delete": uploadGetDelete,
3436
"shared/models/generate_content_stream": generateContentStream,
3537
"shared/tunings/create_get_cancel": createGetCancelTunings,
38+
"file_search_stores/multimodal_flow": multimodalSearchFlow,
3639
}
3740

3841
func wrapResults(resp any, err error) []reflect.Value {
@@ -303,3 +306,122 @@ func generateContentStream(ctx context.Context, client *Client, item *testTableI
303306
}
304307
return wrapResults(lastResp, nil)
305308
}
309+
310+
func multimodalSearchFlow(ctx context.Context, client *Client, item *testTableItem) []reflect.Value {
311+
params := struct {
312+
DisplayName string `json:"displayName"`
313+
Query string `json:"query"`
314+
TextContent string `json:"textContent"`
315+
ImageRelativePath string `json:"imageRelativePath"`
316+
}{}
317+
paramsJSON, _ := json.Marshal(item.Parameters)
318+
if err := json.Unmarshal(paramsJSON, &params); err != nil {
319+
return wrapResults(nil, err)
320+
}
321+
322+
store, err := client.FileSearchStores.Create(ctx, &CreateFileSearchStoreConfig{
323+
DisplayName: params.DisplayName,
324+
})
325+
if err != nil {
326+
return wrapResults(nil, err)
327+
}
328+
329+
defer func() {
330+
trueVar := true
331+
client.FileSearchStores.Delete(ctx, store.Name, &DeleteFileSearchStoreConfig{Force: &trueVar}) // nolint:errcheck
332+
}()
333+
334+
// Upload Text
335+
textFilePath := "tests/data/test_file.txt"
336+
if err := os.MkdirAll(filepath.Dir(textFilePath), 0755); err != nil {
337+
return wrapResults(nil, err)
338+
}
339+
if err := os.WriteFile(textFilePath, []byte(params.TextContent), 0644); err != nil {
340+
return wrapResults(nil, err)
341+
}
342+
defer os.Remove(textFilePath)
343+
344+
opText, err := client.FileSearchStores.UploadToFileSearchStoreFromPath(ctx, textFilePath, store.Name, &UploadToFileSearchStoreConfig{
345+
MIMEType: "text/plain",
346+
})
347+
if err != nil {
348+
return wrapResults(nil, err)
349+
}
350+
351+
// Upload Image
352+
// Resolve path relative to google3
353+
currentDir, _ := os.Getwd()
354+
google3Path := ""
355+
lastIndex := strings.LastIndex(currentDir, "google3/")
356+
if lastIndex != -1 {
357+
google3Path = currentDir[:lastIndex+len("google3/")]
358+
}
359+
resolvedImagePath := filepath.Join(google3Path, "third_party/py/google/genai/tests/data/dog.jpg")
360+
361+
opImage, err := client.FileSearchStores.UploadToFileSearchStoreFromPath(ctx, resolvedImagePath, store.Name, &UploadToFileSearchStoreConfig{
362+
MIMEType: "image/png",
363+
})
364+
if err != nil {
365+
return wrapResults(nil, err)
366+
}
367+
368+
// Wait for operations
369+
for !opText.Done {
370+
time.Sleep(1 * time.Second)
371+
opText, err = client.Operations.GetUploadToFileSearchStoreOperation(ctx, opText, nil)
372+
if err != nil {
373+
return wrapResults(nil, err)
374+
}
375+
}
376+
377+
for !opImage.Done {
378+
time.Sleep(1 * time.Second)
379+
opImage, err = client.Operations.GetUploadToFileSearchStoreOperation(ctx, opImage, nil)
380+
if err != nil {
381+
return wrapResults(nil, err)
382+
}
383+
}
384+
385+
// Search
386+
response, err := client.Models.GenerateContent(ctx, "gemini-2.5-flash", Text(params.Query), &GenerateContentConfig{
387+
Tools: []*Tool{
388+
{
389+
FileSearch: &FileSearch{
390+
FileSearchStoreNames: []string{store.Name},
391+
},
392+
},
393+
},
394+
})
395+
if err != nil {
396+
return wrapResults(nil, err)
397+
}
398+
399+
// Verify response has grounding metadata
400+
if len(response.Candidates) == 0 || response.Candidates[0].GroundingMetadata == nil {
401+
return wrapResults(nil, fmt.Errorf("no grounding metadata in response"))
402+
}
403+
404+
// Download Media
405+
var blobMediaId string
406+
for _, chunk := range response.Candidates[0].GroundingMetadata.GroundingChunks {
407+
if chunk.RetrievedContext != nil && chunk.RetrievedContext.MediaID != "" {
408+
blobMediaId = chunk.RetrievedContext.MediaID
409+
break
410+
}
411+
}
412+
413+
if client.clientConfig.Backend != BackendVertexAI {
414+
if blobMediaId == "" {
415+
return wrapResults(nil, fmt.Errorf("no mediaId found in grounding metadata to test download"))
416+
}
417+
content, err := client.FileSearchStores.DownloadMedia(ctx, blobMediaId, nil)
418+
if err != nil {
419+
return wrapResults(nil, err)
420+
}
421+
if content == nil {
422+
return wrapResults(nil, fmt.Errorf("downloaded content is null"))
423+
}
424+
}
425+
426+
return wrapResults(response, nil)
427+
}

types.go

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)