diff --git a/go/plugins/googlegenai/gemini.go b/go/plugins/googlegenai/gemini.go index 8ff025135e..dabae7f07b 100644 --- a/go/plugins/googlegenai/gemini.go +++ b/go/plugins/googlegenai/gemini.go @@ -276,7 +276,10 @@ func generate( if err != nil { return nil, err } - r := translateResponse(resp) + r, err := translateResponse(resp) + if err != nil { + return nil, err + } r.Request = input if cache != nil { @@ -298,8 +301,11 @@ func generate( return nil, err } for i, c := range chunk.Candidates { - tc := translateCandidate(c) - err := cb(ctx, &ai.ModelResponseChunk{ + tc, err := translateCandidate(c) + if err != nil { + return nil, err + } + err = cb(ctx, &ai.ModelResponseChunk{ Content: tc.Message.Content, }) if err != nil { @@ -322,7 +328,10 @@ func generate( }, } resp.Candidates = merged - r = translateResponse(resp) + r, err = translateResponse(resp) + if err != nil { + return nil, fmt.Errorf("failed to generate contents: %w", err) + } r.Request = input if cache != nil { r.Message.Metadata = cacheMetadata(r.Message.Metadata, cache) @@ -339,11 +348,17 @@ func toGeminiRequest(input *ai.ModelRequest, cache *genai.CachedContent) (*genai return nil, err } - // only one candidate is supported by default - gcc.CandidateCount = 1 + // candidate count might not be set to 1 and will keep its zero value if not set + // e.g. default value from reflection server is 0 + if gcc.CandidateCount == 0 { + gcc.CandidateCount = 1 + } // Genkit primitive fields must be used instead of go-genai fields // i.e.: system prompt, tools, cached content, response schema, etc + if gcc.CandidateCount != 1 { + return nil, errors.New("multiple candidates is not supported") + } if gcc.SystemInstruction != nil { return nil, errors.New("system instruction must be set using Genkit feature: ai.WithSystemPrompt()") } @@ -596,7 +611,7 @@ func toGeminiToolChoice(toolChoice ai.ToolChoice, tools []*ai.ToolDefinition) (* } // translateCandidate translates from a genai.GenerateContentResponse to an ai.ModelResponse. -func translateCandidate(cand *genai.Candidate) *ai.ModelResponse { +func translateCandidate(cand *genai.Candidate) (*ai.ModelResponse, error) { m := &ai.ModelResponse{} switch cand.FinishReason { case genai.FinishReasonStop: @@ -613,6 +628,10 @@ func translateCandidate(cand *genai.Candidate) *ai.ModelResponse { m.FinishReason = ai.FinishReasonUnknown } msg := &ai.Message{} + if cand.Content == nil { + return nil, fmt.Errorf("no valid candidates were found in the generate response") + } + msg.Role = ai.Role(cand.Content.Role) // iterate over the candidate parts, only one struct member @@ -668,14 +687,19 @@ func translateCandidate(cand *genai.Candidate) *ai.ModelResponse { msg.Content = append(msg.Content, p) } m.Message = msg - return m + return m, nil } // Translate from a genai.GenerateContentResponse to a ai.ModelResponse. -func translateResponse(resp *genai.GenerateContentResponse) *ai.ModelResponse { +func translateResponse(resp *genai.GenerateContentResponse) (*ai.ModelResponse, error) { var r *ai.ModelResponse + var err error if len(resp.Candidates) > 0 { - r = translateCandidate(resp.Candidates[0]) + r, err = translateCandidate(resp.Candidates[0]) + if err != nil { + return nil, err + } + } else { r = &ai.ModelResponse{} } @@ -691,7 +715,7 @@ func translateResponse(resp *genai.GenerateContentResponse) *ai.ModelResponse { r.Usage.CachedContentTokens = int(u.CachedContentTokenCount) r.Usage.ThoughtsTokens = int(u.ThoughtsTokenCount) } - return r + return r, nil } // toGeminiParts converts a slice of [ai.Part] to a slice of [genai.Part]. diff --git a/go/plugins/googlegenai/vertexai_live_test.go b/go/plugins/googlegenai/vertexai_live_test.go index 0618879fee..4a9026c616 100644 --- a/go/plugins/googlegenai/vertexai_live_test.go +++ b/go/plugins/googlegenai/vertexai_live_test.go @@ -36,16 +36,16 @@ func TestVertexAILive(t *testing.T) { if !ok { t.Skipf("GOOGLE_CLOUD_PROJECT env var not set") } - location, ok := requireEnv("GOOGLE_CLOUD_LOCATION") + region, ok := requireEnv("GOOGLE_CLOUD_LOCATION") if !ok { t.Log("GOOGLE_CLOUD_LOCATION env var not set, defaulting to us-central1") - location = "us-central1" + region = "us-central1" } ctx := context.Background() g := genkit.Init(ctx, genkit.WithDefaultModel("vertexai/gemini-2.0-flash"), - genkit.WithPlugins(&googlegenai.VertexAI{ProjectID: projectID, Location: location}), + genkit.WithPlugins(&googlegenai.VertexAI{ProjectID: projectID, Location: region}), ) embedder := googlegenai.VertexAIEmbedder(g, "textembedding-gecko@003") @@ -264,8 +264,8 @@ func TestVertexAILive(t *testing.T) { } }) t.Run("image generation", func(t *testing.T) { - if location != "global" { - t.Skipf("image generation in Vertex AI is only supported in region: global, got: %s", location) + if region != "global" { + t.Skipf("image generation in Vertex AI is only supported in region: global, got: %s", region) } m := googlegenai.VertexAIModel(g, "gemini-2.0-flash-preview-image-generation") resp, err := genkit.Generate(ctx, g, @@ -328,8 +328,8 @@ func TestVertexAILive(t *testing.T) { } }) t.Run("thinking enabled", func(t *testing.T) { - if location != "global" && location != "us-central1" { - t.Skipf("thinking in Vertex AI is only supported in these regions: [global, us-central1], got: %q", location) + if region != "global" && region != "us-central1" { + t.Skipf("thinking in Vertex AI is only supported in these regions: [global, us-central1], got: %q", region) } m := googlegenai.VertexAIModel(g, "gemini-2.5-flash-preview-05-20") @@ -360,8 +360,8 @@ func TestVertexAILive(t *testing.T) { } }) t.Run("thinking disabled", func(t *testing.T) { - if location != "global" && location != "us-central1" { - t.Skipf("thinking in Vertex AI is only supported in these regions: [global, us-central1], got: %q", location) + if region != "global" && region != "us-central1" { + t.Skipf("thinking in Vertex AI is only supported in these regions: [global, us-central1], got: %q", region) } m := googlegenai.VertexAIModel(g, "gemini-2.5-flash-preview-05-20") diff --git a/go/plugins/ollama/ollama_live_test.go b/go/plugins/ollama/ollama_live_test.go index 1c16886e02..ba05892838 100644 --- a/go/plugins/ollama/ollama_live_test.go +++ b/go/plugins/ollama/ollama_live_test.go @@ -26,16 +26,17 @@ import ( ollamaPlugin "github.com/firebase/genkit/go/plugins/ollama" ) -var serverAddress = flag.String("server-address", "http://localhost:11434", "Ollama server address") -var modelName = flag.String("model-name", "tinyllama", "model name") -var testLive = flag.Bool("test-live", false, "run live tests") +var ( + serverAddress = flag.String("server-address", "http://localhost:11434", "Ollama server address") + modelName = flag.String("model-name", "tinyllama", "model name") + testLive = flag.Bool("test-live", false, "run live tests") +) /* To run this test, you need to have the Ollama server running. You can set the server address using the OLLAMA_SERVER_ADDRESS environment variable. If the environment variable is not set, the test will default to http://localhost:11434 (the default address for the Ollama server). */ func TestLive(t *testing.T) { - if !*testLive { t.Skip("skipping go/plugins/ollama live test") } diff --git a/go/plugins/vertexai/modelgarden/models.go b/go/plugins/vertexai/modelgarden/models.go index 76c7fef800..d8b80ffdb4 100644 --- a/go/plugins/vertexai/modelgarden/models.go +++ b/go/plugins/vertexai/modelgarden/models.go @@ -18,7 +18,6 @@ package modelgarden import ( "github.com/firebase/genkit/go/ai" - "github.com/firebase/genkit/go/plugins/internal" ) const provider = "vertexai" @@ -26,43 +25,106 @@ const provider = "vertexai" // supported anthropic models var anthropicModels = map[string]ai.ModelOptions{ "claude-3-5-sonnet-v2": { - Label: "Vertex AI Model Garden - Claude 3.5 Sonnet", - Supports: &internal.Multimodal, + Label: "Vertex AI Model Garden - Claude 3.5 Sonnet", + Supports: &ai.ModelSupports{ + Multiturn: true, + Tools: true, + ToolChoice: true, + SystemRole: true, + Media: true, + Constrained: ai.ConstrainedSupportNone, + }, Versions: []string{"claude-3-5-sonnet-v2@20241022"}, }, "claude-3-5-sonnet": { - Label: "Vertex AI Model Garden - Claude 3.5 Sonnet", - Supports: &internal.Multimodal, + Label: "Vertex AI Model Garden - Claude 3.5 Sonnet", + Supports: &ai.ModelSupports{ + Multiturn: true, + Tools: true, + ToolChoice: true, + SystemRole: true, + Media: true, + Constrained: ai.ConstrainedSupportNone, + }, + Versions: []string{"claude-3-5-sonnet@20240620"}, }, "claude-3-sonnet": { - Label: "Vertex AI Model Garden - Claude 3 Sonnet", - Supports: &internal.Multimodal, + Label: "Vertex AI Model Garden - Claude 3 Sonnet", + Supports: &ai.ModelSupports{ + Multiturn: true, + Tools: true, + ToolChoice: true, + SystemRole: true, + Media: true, + Constrained: ai.ConstrainedSupportNone, + }, + Versions: []string{"claude-3-sonnet@20240229"}, }, "claude-3-haiku": { - Label: "Vertex AI Model Garden - Claude 3 Haiku", - Supports: &internal.Multimodal, + Label: "Vertex AI Model Garden - Claude 3 Haiku", + Supports: &ai.ModelSupports{ + Multiturn: true, + Tools: true, + ToolChoice: true, + SystemRole: true, + Media: true, + Constrained: ai.ConstrainedSupportNone, + }, + Versions: []string{"claude-3-haiku@20240307"}, }, "claude-3-opus": { - Label: "Vertex AI Model Garden - Claude 3 Opus", - Supports: &internal.Multimodal, + Label: "Vertex AI Model Garden - Claude 3 Opus", + Supports: &ai.ModelSupports{ + Multiturn: true, + Tools: true, + ToolChoice: true, + SystemRole: true, + Media: true, + Constrained: ai.ConstrainedSupportNone, + }, + Versions: []string{"claude-3-opus@20240229"}, }, "claude-3-7-sonnet": { - Label: "Vertex AI Model Garden - Claude 3.7 Sonnet", - Supports: &internal.Multimodal, + Label: "Vertex AI Model Garden - Claude 3.7 Sonnet", + Supports: &ai.ModelSupports{ + Multiturn: true, + Tools: true, + ToolChoice: true, + SystemRole: true, + Media: true, + Constrained: ai.ConstrainedSupportNone, + }, + Versions: []string{"claude-3-7-sonnet@20250219"}, }, "claude-opus-4": { - Label: "Vertex AI Model Garden - Claude Opus 4", - Supports: &internal.Multimodal, + Label: "Vertex AI Model Garden - Claude Opus 4", + Supports: &ai.ModelSupports{ + Multiturn: true, + Tools: true, + ToolChoice: true, + SystemRole: true, + Media: true, + Constrained: ai.ConstrainedSupportNone, + }, + Versions: []string{"claude-opus-4@20250514"}, }, "claude-sonnet-4": { - Label: "Vertex AI Model Garden - Claude Sonnet 4", - Supports: &internal.Multimodal, + Label: "Vertex AI Model Garden - Claude Sonnet 4", + Supports: &ai.ModelSupports{ + Multiturn: true, + Tools: true, + ToolChoice: true, + SystemRole: true, + Media: true, + Constrained: ai.ConstrainedSupportNone, + }, + Versions: []string{"claude-sonnet-4@20250514"}, }, } diff --git a/go/samples/basic-gemini-with-context/main.go b/go/samples/basic-gemini-with-context/main.go deleted file mode 100644 index 47c9f01af7..0000000000 --- a/go/samples/basic-gemini-with-context/main.go +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright 2025 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 main - -import ( - "context" - "fmt" - - "github.com/firebase/genkit/go/ai" - "github.com/firebase/genkit/go/genkit" - "github.com/firebase/genkit/go/plugins/googlegenai" - "google.golang.org/genai" -) - -func main() { - ctx := context.Background() - - // Initialize Genkit with the Google AI plugin. When you pass nil for the - // Config parameter, the Google AI plugin will get the API key from the - // GEMINI_API_KEY or GOOGLE_API_KEY environment variable, which is the recommended - // practice. - g := genkit.Init(ctx, genkit.WithPlugins(&googlegenai.GoogleAI{})) - - // Define a simple flow that generates jokes about a given topic with a context of bananas - genkit.DefineFlow(g, "contextFlow", func(ctx context.Context, input string) (string, error) { - resp, err := genkit.Generate(ctx, g, - ai.WithModelName("googleai/gemini-2.0-flash"), - ai.WithConfig(&genai.GenerateContentConfig{ - Temperature: genai.Ptr[float32](1.0), - }), - ai.WithPrompt(fmt.Sprintf(`Tell silly short jokes about %s`, input)), - ai.WithDocs(ai.DocumentFromText("Bananas are plentiful in the tropics.", nil))) - if err != nil { - return "", err - } - - text := resp.Text() - return text, nil - }) - - <-ctx.Done() -} diff --git a/go/samples/basic-gemini/main.go b/go/samples/basic-gemini/main.go deleted file mode 100644 index 756dbaad09..0000000000 --- a/go/samples/basic-gemini/main.go +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2025 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 main - -import ( - "context" - - "github.com/firebase/genkit/go/ai" - "github.com/firebase/genkit/go/genkit" - "github.com/firebase/genkit/go/plugins/googlegenai" - "google.golang.org/genai" -) - -func main() { - ctx := context.Background() - - // Initialize Genkit with the Google AI plugin. When you pass nil for the - // Config parameter, the Google AI plugin will get the API key from the - // GEMINI_API_KEY or GOOGLE_API_KEY environment variable, which is the recommended - // practice. - g := genkit.Init(ctx, genkit.WithPlugins(&googlegenai.GoogleAI{})) - - // Define a simple flow that generates jokes about a given topic - genkit.DefineStreamingFlow(g, "jokesFlow", func(ctx context.Context, input string, cb ai.ModelStreamCallback) (string, error) { - resp, err := genkit.Generate(ctx, g, - ai.WithModelName("googleai/gemini-2.5-flash"), - ai.WithConfig(&genai.GenerateContentConfig{ - Temperature: genai.Ptr[float32](1.0), - ThinkingConfig: &genai.ThinkingConfig{ - ThinkingBudget: genai.Ptr[int32](0), - }, - }), - ai.WithStreaming(cb), - ai.WithPrompt(`Tell short jokes about %s`, input)) - if err != nil { - return "", err - } - - return resp.Text(), nil - }) - - <-ctx.Done() -} diff --git a/go/samples/cache-gemini/main.go b/go/samples/cache-gemini/main.go deleted file mode 100644 index 69e3b71b16..0000000000 --- a/go/samples/cache-gemini/main.go +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright 2025 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. -// -// SPDX-License-Identifier: Apache-2.0 - -package main - -import ( - "context" - "errors" - - "os" - - "github.com/firebase/genkit/go/ai" - "github.com/firebase/genkit/go/genkit" - "github.com/firebase/genkit/go/plugins/googlegenai" - "google.golang.org/genai" -) - -// duneQuestionInput is a question about Dune. -type duneQuestionInput struct { - Question string `json:"question"` - FilePath string `json:"path"` -} - -func main() { - ctx := context.Background() - g := genkit.Init(ctx, - genkit.WithDefaultModel("googleai/gemini-2.5-flash-preview-04-17"), - genkit.WithPlugins(&googlegenai.GoogleAI{}), - ) - - genkit.DefineFlow(g, "duneFlowGeminiAI", func(ctx context.Context, input *duneQuestionInput) (string, error) { - prompt := "What is the text I provided you with?" - if input == nil { - return "", errors.New("empty flow input, provide at least a source file to read") - } - if len(input.Question) > 0 { - prompt = input.Question - } - - textContent, err := os.ReadFile(input.FilePath) - if err != nil { - return "", err - } - - // generate a request with a large text content to be cached - resp, err := genkit.Generate(ctx, g, ai.WithConfig(&genai.GenerateContentConfig{ - Temperature: genai.Ptr[float32](0.7), - }), - ai.WithMessages( - ai.NewUserTextMessage(string(textContent)).WithCacheTTL(360), - ), - ai.WithPrompt(prompt), - ) - if err != nil { - return "", nil - } - // use previous messages to keep the conversation going and keep - // asking questions related to the large content that was cached - resp, err = genkit.Generate(ctx, g, ai.WithConfig(&genai.GenerateContentConfig{ - Temperature: genai.Ptr[float32](0.7), - }), - ai.WithMessages(resp.History()...), - ai.WithPrompt("now rewrite the previous summary and make it look like a pirate wrote it"), - ) - if err != nil { - return "", nil - } - - text := resp.Text() - return text, nil - }) - - <-ctx.Done() -} diff --git a/go/samples/code-execution-gemini/main.go b/go/samples/code-execution-gemini/main.go deleted file mode 100644 index 0162e23276..0000000000 --- a/go/samples/code-execution-gemini/main.go +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright 2025 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 main - -import ( - "context" - "errors" - "fmt" - - "strings" - - "github.com/firebase/genkit/go/ai" - "github.com/firebase/genkit/go/genkit" - "github.com/firebase/genkit/go/plugins/googlegenai" - "google.golang.org/genai" -) - -func main() { - ctx := context.Background() - - // Initialize Genkit with Google AI plugin - g := genkit.Init(ctx, genkit.WithPlugins(&googlegenai.GoogleAI{})) - - // Define a flow to demonstrate code execution - genkit.DefineFlow(g, "codeExecutionFlow", func(ctx context.Context, _ any) (string, error) { - m := googlegenai.GoogleAIModel(g, "gemini-2.5-flash-preview-04-17") - if m == nil { - return "", errors.New("failed to find model") - } - - problem := "find the sum of first 5 prime numbers" - fmt.Printf("Problem: %s\n", problem) - - // Generate response with code execution enabled - fmt.Println("Sending request to Gemini...") - resp, err := genkit.Generate(ctx, g, - ai.WithModel(m), - ai.WithConfig(&genai.GenerateContentConfig{ - Temperature: genai.Ptr[float32](0.2), - Tools: []*genai.Tool{ - { - CodeExecution: &genai.ToolCodeExecution{}, - }, - }, - }), - ai.WithPrompt(problem)) - if err != nil { - return "", err - } - - // You can also use the helper function for simpler code - fmt.Println("\n=== INTERNAL CODE EXECUTION ===") - displayCodeExecution(resp.Message) - - fmt.Println("\n=== COMPLETE INTERNAL CODE EXECUTION ===") - text := resp.Text() - fmt.Println(text) - - return text, nil - }) - <-ctx.Done() -} - -// DisplayCodeExecution prints the code execution results from a message in a formatted way. -// This is a helper for applications that want to display code execution results to users. -func displayCodeExecution(msg *ai.Message) { - // Extract and display executable code - code := googlegenai.GetExecutableCode(msg) - fmt.Printf("Language: %s\n", code.Language) - fmt.Printf("```%s\n%s\n```\n", code.Language, code.Code) - - // Extract and display execution results - result := googlegenai.GetCodeExecutionResult(msg) - fmt.Printf("\nExecution result:\n") - fmt.Printf("Status: %s\n", result.Outcome) - fmt.Printf("Output:\n") - if strings.TrimSpace(result.Output) == "" { - fmt.Printf(" \n") - } else { - lines := strings.SplitSeq(result.Output, "\n") - for line := range lines { - fmt.Printf(" %s\n", line) - } - } - - // Display any explanatory text - for _, part := range msg.Content { - if part.IsText() { - fmt.Printf("\nExplanation:\n%s\n", part.Text) - } - } -} diff --git a/go/samples/compat_oai/anthropic/main.go b/go/samples/compat_oai/anthropic/main.go index 57cc2ff64f..e4c3ea2bd2 100644 --- a/go/samples/compat_oai/anthropic/main.go +++ b/go/samples/compat_oai/anthropic/main.go @@ -32,15 +32,15 @@ func main() { } g := genkit.Init(ctx, genkit.WithPlugins(&oai)) - genkit.DefineFlow(g, "anthropic", func(ctx context.Context, subject string) (string, error) { + genkit.DefineFlow(g, "joke-teller", func(ctx context.Context, subject string) (string, error) { sonnet37 := oai.Model(g, "claude-3-7-sonnet-20250219") prompt := fmt.Sprintf("tell me a joke about %s", subject) - foo, err := genkit.Generate(ctx, g, ai.WithModel(sonnet37), ai.WithPrompt(prompt)) + r, err := genkit.Generate(ctx, g, ai.WithModel(sonnet37), ai.WithPrompt(prompt)) if err != nil { return "", err } - return fmt.Sprintf("foo: %s", foo.Text()), nil + return r.Text(), nil }) mux := http.NewServeMux() diff --git a/go/samples/gemini/README.md b/go/samples/gemini/README.md new file mode 100644 index 0000000000..7f5ee69a0c --- /dev/null +++ b/go/samples/gemini/README.md @@ -0,0 +1,49 @@ +# Gemini samples + +This folder contains several Gemini small applications using [Genkit Flows](https://genkit.dev/docs/flows/?lang=go) and GoogleAI and VertexAI plugins: + +[!TIP] +Samples are interchangeable between plugins + +## `joke-teller` + +A basic joke generator from a given topic + +## `context` + +A basic joke generator from a given topic with a specific context given + +## `comic-strip-generator` + +A small application that uses Nano Banana to generate a brief comic strip from a +default input or any given topic + +## `cached-contents` + +A small application that caches Romeo and Juliet's book and answers questions +about it + +## `code-execution` + +A flow that demonstrates how to perform code execution in Gemini models + +## `image-descriptor` + +A flow that prompts the model an image and request a brief description of it + +## `image-generator` + +A flow that uses VertexAI to generate an image using imagen-3.0 model + +## `text-to-speech` + +A simple demonstration of how to use a TTS model to convert text to speech + +## `speech-to-text` + +A flow that receives audio as an input and transcribes its contents + +## `assistant-greeting` + +A flow that uses prompts that requests a Gemini model to generate +greetings using a given style diff --git a/go/samples/text-to-speech/genkit.wav b/go/samples/gemini/genkit.wav similarity index 100% rename from go/samples/text-to-speech/genkit.wav rename to go/samples/gemini/genkit.wav diff --git a/go/samples/gemini/main.go b/go/samples/gemini/main.go new file mode 100644 index 0000000000..534e8b0323 --- /dev/null +++ b/go/samples/gemini/main.go @@ -0,0 +1,393 @@ +// Copyright 2025 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 main + +import ( + "context" + "encoding/base64" + "fmt" + "io" + "net/http" + "os" + "strings" + + "github.com/firebase/genkit/go/ai" + "github.com/firebase/genkit/go/genkit" + "github.com/firebase/genkit/go/plugins/googlegenai" + "google.golang.org/genai" +) + +func main() { + ctx := context.Background() + + // Initialize Genkit with the Google AI plugin. When you pass nil for the + // Config parameter, the Google AI plugin will get the API key from the + // GEMINI_API_KEY or GOOGLE_API_KEY environment variable, which is the recommended + // practice. + g := genkit.Init(ctx, + genkit.WithPlugins(&googlegenai.GoogleAI{}), + genkit.WithDefaultModel("googleai/gemini-2.5-flash")) + + // Define a simple flow that generates jokes about a given topic + genkit.DefineFlow(g, "joke-teller", func(ctx context.Context, input string) (string, error) { + resp, err := genkit.Generate(ctx, g, + ai.WithConfig(&genai.GenerateContentConfig{ + Temperature: genai.Ptr[float32](1.0), + ThinkingConfig: &genai.ThinkingConfig{ + ThinkingBudget: genai.Ptr[int32](0), + }, + }), + ai.WithPrompt("Tell short jokes about %s", input)) + if err != nil { + return "", err + } + + return resp.Text(), nil + }) + + // Define a simple flow that generates jokes about a given topic with a context of bananas + genkit.DefineFlow(g, "context", func(ctx context.Context, input string) (string, error) { + resp, err := genkit.Generate(ctx, g, + ai.WithModelName("googleai/gemini-2.0-flash"), + ai.WithConfig(&genai.GenerateContentConfig{ + Temperature: genai.Ptr[float32](1.0), + }), + ai.WithPrompt("Tell short jokes about %s", input), + ai.WithDocs(ai.DocumentFromText("Bananas are plentiful in the tropics.", nil))) + if err != nil { + return "", err + } + + text := resp.Text() + return text, nil + }) + + // Simple flow that generates a brief comic strip + genkit.DefineFlow(g, "comic-strip-generator", func(ctx context.Context, input string) ([]string, error) { + if input == "" { + input = `A little blue gopher with big eyes trying to learn Python, + use a cartoon style, the story should be tragic because he + chose the wrong programming language, the proper programing + language for a gopher should be Go` + } + resp, err := genkit.Generate(ctx, g, + ai.WithModelName("googleai/gemini-2.5-flash-image-preview"), // nano banana + ai.WithConfig(&genai.GenerateContentConfig{ + Temperature: genai.Ptr[float32](0.5), + ResponseModalities: []string{"IMAGE", "TEXT"}, + }), + ai.WithPrompt("generate a short story about %s and for each scene, generate an image for it", input)) + if err != nil { + return nil, err + } + + story := []string{} + for _, p := range resp.Message.Content { + if p.IsMedia() || p.IsText() { + story = append(story, p.Text) + } + } + + return story, nil + }) + + // A flow that uses Romeo and Juliet as cache contents to answer questions about the book + genkit.DefineFlow(g, "cached-contents", func(ctx context.Context, input string) (string, error) { + // Romeo and Juliet + url := "https://www.gutenberg.org/cache/epub/1513/pg1513.txt" + prompt := "I'll provide you with some text contents that I want you to use to answer further questions" + content, err := readTextFromURL(url) + if err != nil { + return "", err + } + + resp, err := genkit.Generate(ctx, g, + ai.WithConfig(&genai.GenerateContentConfig{ + Temperature: genai.Ptr[float32](1.0), + ThinkingConfig: &genai.ThinkingConfig{ + ThinkingBudget: genai.Ptr[int32](0), + }, + }), + ai.WithMessages( + ai.NewUserTextMessage(content).WithCacheTTL(360), // create cache contents + ), + ai.WithPrompt(prompt)) + if err != nil { + return "", err + } + + // use previous messages to keep the conversation going and + // ask questions related to the cached content + prompt = "Write a brief summary of the character development of Juliet" + if input != "" { + prompt = input + } + resp, err = genkit.Generate(ctx, g, + ai.WithConfig(&genai.GenerateContentConfig{ + Temperature: genai.Ptr[float32](1.0), + ThinkingConfig: &genai.ThinkingConfig{ + ThinkingBudget: genai.Ptr[int32](0), + }, + }), + ai.WithMessages(resp.History()...), + ai.WithPrompt(prompt)) + if err != nil { + return "", nil + } + return resp.Text(), nil + }) + + // Define a flow to demonstrate code execution + genkit.DefineFlow(g, "code-execution", func(ctx context.Context, _ any) (string, error) { + m := googlegenai.GoogleAIModel(g, "gemini-2.5-flash") + if m == nil { + return "", fmt.Errorf("failed to find model") + } + + problem := "find the sum of first 5 prime numbers" + fmt.Printf("Problem: %s\n", problem) + + // Generate response with code execution enabled + fmt.Println("Sending request to Gemini...") + resp, err := genkit.Generate(ctx, g, + ai.WithModel(m), + ai.WithConfig(&genai.GenerateContentConfig{ + Temperature: genai.Ptr[float32](0.2), + Tools: []*genai.Tool{ + { + CodeExecution: &genai.ToolCodeExecution{}, + }, + }, + }), + ai.WithPrompt(problem)) + if err != nil { + return "", err + } + + // You can also use the helper function for simpler code + fmt.Println("\n=== INTERNAL CODE EXECUTION ===") + displayCodeExecution(resp.Message) + + fmt.Println("\n=== COMPLETE INTERNAL CODE EXECUTION ===") + text := resp.Text() + fmt.Println(text) + + return text, nil + }) + + genkit.DefineFlow(g, "image-descriptor", func(ctx context.Context, foo string) (string, error) { + img, err := fetchImgAsBase64() + if err != nil { + return "", err + } + resp, err := genkit.Generate(ctx, g, + ai.WithConfig(&genai.GenerateContentConfig{ + Temperature: genai.Ptr[float32](1.0), + }), + ai.WithMessages(ai.NewUserMessage( + ai.NewTextPart("Can you describe what's in this image?"), + ai.NewMediaPart("image/jpeg", "data:image/jpeg;base64,"+img)), + )) + if err != nil { + return "", err + } + + text := resp.Text() + return text, nil + }) + + genkit.DefineFlow(g, "image-generation", func(ctx context.Context, input string) ([]string, error) { + r, err := genkit.Generate(ctx, g, + ai.WithModelName("googleai/imagen-4.0-generate-001"), + ai.WithPrompt("Generate an image of %s", input), + ai.WithConfig(&genai.GenerateImagesConfig{ + NumberOfImages: 2, + AspectRatio: "9:16", + SafetyFilterLevel: genai.SafetyFilterLevelBlockLowAndAbove, + PersonGeneration: genai.PersonGenerationAllowAll, + OutputMIMEType: "image/jpeg", + }), + ) + if err != nil { + return nil, err + } + + var images []string + for _, m := range r.Message.Content { + images = append(images, m.Text) + } + return images, nil + }) + + // Define a simple flow that generates audio transcripts from a given audio + genkit.DefineFlow(g, "speech-to-text-flow", func(ctx context.Context, input any) (string, error) { + audio, err := os.Open("./genkit.wav") + if err != nil { + return "", err + } + defer audio.Close() + + audioBytes, err := io.ReadAll(audio) + if err != nil { + return "", err + } + resp, err := genkit.Generate(ctx, g, + ai.WithModelName("googleai/gemini-2.5-flash"), + ai.WithMessages(ai.NewUserMessage( + ai.NewTextPart("Can you transcribe the next audio?"), + ai.NewMediaPart("audio/wav", "data:audio/wav;base64,"+base64.StdEncoding.EncodeToString(audioBytes)))), + ) + if err != nil { + return "", err + } + + return resp.Text(), nil + }) + + // Simple flow that generates an audio from a given text + genkit.DefineFlow(g, "text-to-speech-flow", func(ctx context.Context, input string) (string, error) { + prompt := "Genkit is the best Gen AI library!" + if input != "" { + prompt = input + } + resp, err := genkit.Generate(ctx, g, + ai.WithConfig(&genai.GenerateContentConfig{ + Temperature: genai.Ptr[float32](1.0), + ResponseModalities: []string{"AUDIO"}, + SpeechConfig: &genai.SpeechConfig{ + VoiceConfig: &genai.VoiceConfig{ + PrebuiltVoiceConfig: &genai.PrebuiltVoiceConfig{ + VoiceName: "Algenib", + }, + }, + }, + }), + ai.WithModelName("googleai/gemini-2.5-flash-preview-tts"), + ai.WithPrompt("Say: %s", prompt)) + if err != nil { + return "", err + } + + // base64 encoded audio + return resp.Text(), nil + }) + + type greetingStyle struct { + Style string `json:"style"` + Location string `json:"location"` + Name string `json:"name"` + } + + type greeting struct { + Greeting string `json:"greeting"` + } + + // Define a simple flow that prompts an LLM to generate greetings using a + // given style. + genkit.DefineFlow(g, "assistant-greeting", func(ctx context.Context, input greetingStyle) (string, error) { + // Look up the prompt by name + prompt := genkit.LookupPrompt(g, "example") + if prompt == nil { + return "", fmt.Errorf("assistantreetingFlow: failed to find prompt") + } + + // Execute the prompt with the provided input + resp, err := prompt.Execute(ctx, ai.WithInput(input)) + if err != nil { + return "", err + } + + var output greeting + if err = resp.Output(&output); err != nil { + return "", err + } + + return output.Greeting, nil + }) + + <-ctx.Done() +} + +// Helper functions + +// readTextFromURL reads the text contents from a given URL +func readTextFromURL(url string) (string, error) { + resp, err := http.Get(url) + if err != nil { + return "", fmt.Errorf("failed to sent HTTP GET request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("non 2XX status code received: %d", resp.StatusCode) + } + + contents, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("failed to read response body: %w", err) + } + + return string(contents), nil +} + +// displayCodeExecution prints the code execution results from a message in a formatted way. +// This is a helper for applications that want to display code execution results to users. +func displayCodeExecution(msg *ai.Message) { + // Extract and display executable code + code := googlegenai.GetExecutableCode(msg) + fmt.Printf("Language: %s\n", code.Language) + fmt.Printf("```%s\n%s\n```\n", code.Language, code.Code) + + // Extract and display execution results + result := googlegenai.GetCodeExecutionResult(msg) + fmt.Printf("\nExecution result:\n") + fmt.Printf("Status: %s\n", result.Outcome) + fmt.Printf("Output:\n") + if strings.TrimSpace(result.Output) == "" { + fmt.Printf(" \n") + } else { + lines := strings.SplitSeq(result.Output, "\n") + for line := range lines { + fmt.Printf(" %s\n", line) + } + } + + // Display any explanatory text + for _, part := range msg.Content { + if part.IsText() { + fmt.Printf("\nExplanation:\n%s\n", part.Text) + } + } +} + +func fetchImgAsBase64() (string, error) { + imgUrl := "https://pd.w.org/2025/07/58268765f177911d4.13750400-2048x1365.jpg" + resp, err := http.Get(imgUrl) + if err != nil { + return "", err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return "", err + } + + imageData, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + + base64string := base64.StdEncoding.EncodeToString(imageData) + return base64string, nil +} diff --git a/go/samples/prompts-dir/prompts/example.prompt b/go/samples/gemini/prompts/example.prompt similarity index 100% rename from go/samples/prompts-dir/prompts/example.prompt rename to go/samples/gemini/prompts/example.prompt diff --git a/go/samples/imagen-gemini/main.go b/go/samples/imagen-gemini/main.go deleted file mode 100644 index 3f803c5f52..0000000000 --- a/go/samples/imagen-gemini/main.go +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright 2025 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 main - -import ( - "context" - "errors" - "fmt" - - "github.com/firebase/genkit/go/ai" - "github.com/firebase/genkit/go/genkit" - "github.com/firebase/genkit/go/plugins/googlegenai" - "google.golang.org/genai" -) - -func main() { - ctx := context.Background() - - // Initialize Genkit with the Google AI plugin. When you pass nil for the - // Config parameter, the Google AI plugin will get the API key from the - // GEMINI_API_KEY or GOOGLE_API_KEY environment variable, which is the recommended - // practice. - g := genkit.Init(ctx, genkit.WithPlugins(&googlegenai.GoogleAI{})) - - // Define a simple flow that generates an image of a given topic - genkit.DefineFlow(g, "imageFlow", func(ctx context.Context, input string) ([]string, error) { - m := googlegenai.GoogleAIModel(g, "gemini-2.0-flash-exp") - if m == nil { - return nil, errors.New("imageFlow: failed to find model") - } - - if input == "" { - input = `A little blue gopher with big eyes trying to learn Python, - use a cartoon style, the story should be tragic because he - chose the wrong programming language, the proper programing - language for a gopher should be Go` - } - resp, err := genkit.Generate(ctx, g, - ai.WithModel(m), - ai.WithConfig(&genai.GenerateContentConfig{ - Temperature: genai.Ptr[float32](0.5), - ResponseModalities: []string{"IMAGE", "TEXT"}, - }), - ai.WithPrompt(fmt.Sprintf(`generate a story about %s and for each scene, generate an image for it`, input))) - if err != nil { - return nil, err - } - - story := []string{} - for _, p := range resp.Message.Content { - if p.IsMedia() || p.IsText() { - story = append(story, p.Text) - } - } - - return story, nil - }) - - <-ctx.Done() -} diff --git a/go/samples/imagen/main.go b/go/samples/imagen/main.go deleted file mode 100644 index 0737e60a82..0000000000 --- a/go/samples/imagen/main.go +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2025 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 main - -import ( - "context" - "log" - - "github.com/firebase/genkit/go/ai" - "github.com/firebase/genkit/go/genkit" - "github.com/firebase/genkit/go/plugins/googlegenai" - "google.golang.org/genai" -) - -func main() { - ctx := context.Background() - g := genkit.Init(ctx, genkit.WithPlugins(&googlegenai.VertexAI{})) - - genkit.DefineFlow(g, "image-generation", func(ctx context.Context, input string) ([]string, error) { - r, err := genkit.Generate(ctx, g, - ai.WithModelName("vertexai/imagen-3.0-generate-001"), - ai.WithPrompt("Generate an image of %s", input), - ai.WithConfig(&genai.GenerateImagesConfig{ - NumberOfImages: 2, - NegativePrompt: "night", - AspectRatio: "9:16", - SafetyFilterLevel: genai.SafetyFilterLevelBlockLowAndAbove, - PersonGeneration: genai.PersonGenerationAllowAll, - Language: genai.ImagePromptLanguageEn, - AddWatermark: true, - OutputMIMEType: "image/jpeg", - }), - ) - if err != nil { - log.Fatal(err) - } - - var images []string - for _, m := range r.Message.Content { - images = append(images, m.Text) - } - return images, nil - }) - - <-ctx.Done() -} diff --git a/go/samples/mcp-client/main.go b/go/samples/mcp-client/main.go index 1864a27c08..b6f9149e26 100644 --- a/go/samples/mcp-client/main.go +++ b/go/samples/mcp-client/main.go @@ -57,7 +57,7 @@ func clientExample() { } response, err := genkit.Generate(ctx, g, - ai.WithModelName("googleai/gemini-2.5-pro-preview-05-06"), + ai.WithModelName("googleai/gemini-2.5-pro"), ai.WithPrompt("Convert the current time from New York to London timezone."), ai.WithTools(toolRefs...), ai.WithToolChoice(ai.ToolChoiceAuto), @@ -109,7 +109,7 @@ func managerExample() { } response, err := genkit.Generate(ctx, g, - ai.WithModelName("googleai/gemini-2.5-pro-preview-05-06"), + ai.WithModelName("googleai/gemini-2.5-pro"), ai.WithPrompt("What time is it in New York and Tokyo?"), ai.WithTools(toolRefs...), ai.WithToolChoice(ai.ToolChoiceAuto), diff --git a/go/samples/modelgarden/main.go b/go/samples/modelgarden/main.go index 8b7ac58f87..2cf5e5be68 100644 --- a/go/samples/modelgarden/main.go +++ b/go/samples/modelgarden/main.go @@ -16,7 +16,11 @@ package main import ( "context" + "encoding/base64" "errors" + "io" + "log" + "net/http" "github.com/firebase/genkit/go/ai" "github.com/firebase/genkit/go/genkit" @@ -29,10 +33,10 @@ func main() { g := genkit.Init(ctx, genkit.WithPlugins(&modelgarden.Anthropic{})) // Define a simple flow that generates jokes about a given topic - genkit.DefineFlow(g, "jokesFlow", func(ctx context.Context, input string) (string, error) { + genkit.DefineFlow(g, "joke-teller", func(ctx context.Context, input string) (string, error) { m := modelgarden.AnthropicModel(g, "claude-3-5-sonnet-v2") if m == nil { - return "", errors.New("jokesFlow: failed to find model") + return "", errors.New("joke-teller: failed to find model") } resp, err := genkit.Generate(ctx, g, @@ -49,5 +53,83 @@ func main() { return text, nil }) + genkit.DefineFlow(g, "image-descriptor", func(ctx context.Context, foo string) (string, error) { + m := modelgarden.AnthropicModel(g, "claude-3-5-sonnet-v2") + if m == nil { + return "", errors.New("image-descriptor: failed to find model") + } + + img, err := fetchImgAsBase64() + if err != nil { + log.Fatal(err) + } + + resp, err := genkit.Generate(ctx, g, + ai.WithModel(m), + ai.WithConfig(&ai.GenerationCommonConfig{ + Temperature: 1.0, + }), + ai.WithMessages(ai.NewUserMessage( + ai.NewTextPart("Can you describe what's in this image?"), + ai.NewMediaPart("image/jpeg", "data:image/jpeg;base64,"+img)), + )) + if err != nil { + return "", err + } + + text := resp.Text() + return text, nil + }) + + type Recipe struct { + Steps []string + Ingredients []string + } + + genkit.DefineFlow(g, "chef-bot", func(ctx context.Context, input string) (Recipe, error) { + m := modelgarden.AnthropicModel(g, "claude-3-5-sonnet-v2") + r := Recipe{} + if m == nil { + return r, errors.New("chef-bot: failed to find model") + } + + resp, err := genkit.Generate(ctx, g, + ai.WithModel(m), + ai.WithConfig(&ai.GenerationCommonConfig{ + Temperature: 1.0, + }), + ai.WithPrompt(`Send me a recipe for %s`, input), + ai.WithOutputType(Recipe{})) + if err != nil { + return r, err + } + + err = resp.Output(&r) + if err != nil { + return r, err + } + return r, nil + }) + <-ctx.Done() } + +func fetchImgAsBase64() (string, error) { + imgUrl := "https://pd.w.org/2025/07/58268765f177911d4.13750400-2048x1365.jpg" + resp, err := http.Get(imgUrl) + if err != nil { + return "", err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return "", err + } + + imageBytes, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + + base64string := base64.StdEncoding.EncodeToString(imageBytes) + return base64string, nil +} diff --git a/go/samples/ollama-tools/main.go b/go/samples/ollama-tools/main.go deleted file mode 100644 index 2dd4dd2cdf..0000000000 --- a/go/samples/ollama-tools/main.go +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright 2025 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 main - -import ( - "context" - "fmt" - "time" - - "github.com/firebase/genkit/go/ai" - "github.com/firebase/genkit/go/genkit" - "github.com/firebase/genkit/go/plugins/ollama" -) - -// WeatherInput defines the input structure for the weather tool -type WeatherInput struct { - Location string `json:"location"` -} - -// WeatherData represents weather information -type WeatherData struct { - Location string `json:"location"` - TempC float64 `json:"temp_c"` - TempF float64 `json:"temp_f"` - Condition string `json:"condition"` -} - -func main() { - ctx := context.Background() - - // Initialize Genkit with the Ollama plugin - ollamaPlugin := &ollama.Ollama{ - ServerAddress: "http://localhost:11434", // Default Ollama server address - Timeout: 60, // Response timeout in seconds - } - - g := genkit.Init(ctx, genkit.WithPlugins(ollamaPlugin)) - - // Define the Ollama model - model := ollamaPlugin.DefineModel(g, - ollama.ModelDefinition{ - Name: "llama3.1", // Choose an appropriate model - Type: "chat", // Must be chat for tool support - }, - nil) - - // Define tools - weatherTool := genkit.DefineTool(g, "weather", "Get current weather for a location", - func(ctx *ai.ToolContext, input WeatherInput) (WeatherData, error) { - // Get weather data (simulated) - return simulateWeather(input.Location), nil - }, - ) - - // Create system message - systemMsg := ai.NewSystemTextMessage( - "You are a helpful assistant that can look up weather. " + - "When providing weather information, use the appropriate tool.") - - // Create user message - userMsg := ai.NewUserTextMessage("I'd like to know the weather in Tokyo.") - - // Generate response with tools - fmt.Println("Generating response with weather tool...") - - resp, err := genkit.Generate(ctx, g, - ai.WithModel(model), - ai.WithMessages(systemMsg, userMsg), - ai.WithTools(weatherTool), - ai.WithToolChoice(ai.ToolChoiceAuto), - ) - if err != nil { - fmt.Printf("Error: %v\n", err) - return - } - - // Print the final response - fmt.Println("\n----- Final Response -----") - fmt.Printf("%s\n", resp.Text()) - fmt.Println("--------------------------") -} - -// simulateWeather returns simulated weather data for a location -func simulateWeather(location string) WeatherData { - // In a real app, this would call a weather API - // For demonstration, we'll return mock data - tempC := 22.5 - if location == "Tokyo" || location == "Tokyo, Japan" { - tempC = 24.0 - } else if location == "Paris" || location == "Paris, France" { - tempC = 18.5 - } else if location == "New York" || location == "New York, USA" { - tempC = 15.0 - } - - conditions := []string{"Sunny", "Partly Cloudy", "Cloudy", "Rainy", "Stormy"} - condition := conditions[time.Now().Unix()%int64(len(conditions))] - - return WeatherData{ - Location: location, - TempC: tempC, - TempF: tempC*9/5 + 32, - Condition: condition, - } -} diff --git a/go/samples/ollama-vision/main.go b/go/samples/ollama-vision/main.go deleted file mode 100644 index e802256789..0000000000 --- a/go/samples/ollama-vision/main.go +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright 2025 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 main - -import ( - "context" - "encoding/base64" - "fmt" - "log" - "net/http" - "os" - "path/filepath" - "strings" - - "github.com/firebase/genkit/go/ai" - "github.com/firebase/genkit/go/genkit" - "github.com/firebase/genkit/go/plugins/ollama" -) - -// getContentTypeFromExtension returns a MIME type based on file extension -func getContentTypeFromExtension(filename string) string { - ext := strings.ToLower(filepath.Ext(filename)) - switch ext { - case ".jpg", ".jpeg": - return "image/jpeg" - case ".png": - return "image/png" - case ".gif": - return "image/gif" - case ".webp": - return "image/webp" - case ".bmp": - return "image/bmp" - case ".svg": - return "image/svg+xml" - default: - return "image/png" // Default fallback - } -} - -func main() { - // Get the image path from command line argument or use a default path - imagePath := "test.png" - if len(os.Args) > 1 { - imagePath = os.Args[1] - } - - // Check if image exists - if _, err := os.Stat(imagePath); os.IsNotExist(err) { - log.Fatalf("Image file not found: %s", imagePath) - } - - // Read the image file - imageData, err := os.ReadFile(imagePath) - if err != nil { - log.Fatalf("Failed to read image file: %v", err) - } - - // Detect content type (MIME type) from the file's binary signature - contentType := http.DetectContentType(imageData) - - // If content type is generic/unknown, try to infer from file extension - if contentType == "application/octet-stream" { - contentType = getContentTypeFromExtension(imagePath) - } - - // Encode image to base64 - base64Image := base64.StdEncoding.EncodeToString(imageData) - dataURI := fmt.Sprintf("data:%s;base64,%s", contentType, base64Image) - - // Create a new Genkit instance - g := genkit.Init(context.Background()) - - // Initialize the Ollama plugin - ollamaPlugin := &ollama.Ollama{ - ServerAddress: "http://localhost:11434", // Default Ollama server address - } - - // Initialize the plugin - actions := ollamaPlugin.Init(context.Background()) - log.Printf("Initialized Ollama plugin with %d actions", len(actions)) - - // Define a model that supports images (llava is one of the supported models) - modelName := "llava" - model := ollamaPlugin.DefineModel(g, ollama.ModelDefinition{ - Name: modelName, - Type: "chat", - }, nil) - - // Create a context - ctx := context.Background() - - // Create a request with text and image - request := &ai.ModelRequest{ - Messages: []*ai.Message{ - { - Role: ai.RoleUser, - Content: []*ai.Part{ - ai.NewTextPart("Describe what you see in this image:"), - ai.NewMediaPart(contentType, dataURI), - }, - }, - }, - } - - // Call the model - fmt.Printf("Sending request to %s model...\n", modelName) - response, err := model.Generate(ctx, request, nil) - if err != nil { - log.Fatalf("Error generating response: %v", err) - } - - // Print the response - fmt.Println("\nModel Response:") - for _, part := range response.Message.Content { - if part.IsText() { - fmt.Println(part.Text) - } - } -} diff --git a/go/samples/ollama/README.md b/go/samples/ollama/README.md new file mode 100644 index 0000000000..a260034f0f --- /dev/null +++ b/go/samples/ollama/README.md @@ -0,0 +1,11 @@ +# Ollama samples + +This folder contains Ollama samples using [Genkit Flows](https://genkit.dev/docs/flows/?lang=go). + +## `tools` + +A simple flow that demonstrates how to use tools using Genkit and Ollama models + +## `vision` + +A flow that uses an Ollama model to describe an image using Genkit diff --git a/go/samples/ollama/main.go b/go/samples/ollama/main.go new file mode 100644 index 0000000000..435dd3cc27 --- /dev/null +++ b/go/samples/ollama/main.go @@ -0,0 +1,204 @@ +// Copyright 2025 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 main + +import ( + "context" + "encoding/base64" + "fmt" + "net/http" + "os" + "path/filepath" + "strings" + "time" + + "github.com/firebase/genkit/go/ai" + "github.com/firebase/genkit/go/genkit" + "github.com/firebase/genkit/go/plugins/ollama" +) + +// WeatherInput defines the input structure for the weather tool +type WeatherInput struct { + Location string `json:"location"` +} + +// WeatherData represents weather information +type WeatherData struct { + Location string `json:"location"` + TempC float64 `json:"temp_c"` + TempF float64 `json:"temp_f"` + Condition string `json:"condition"` +} + +func main() { + ctx := context.Background() + + // Initialize Genkit with the Ollama plugin + ollamaPlugin := &ollama.Ollama{ + ServerAddress: "http://localhost:11434", // Default Ollama server address + Timeout: 90, // Response timeout in seconds + } + + g := genkit.Init(ctx, genkit.WithPlugins(ollamaPlugin)) + + // Define tools + weatherTool := genkit.DefineTool(g, "weather", "Get current weather for a location", + func(ctx *ai.ToolContext, input WeatherInput) (WeatherData, error) { + // Get weather data (simulated) + return simulateWeather(input.Location), nil + }, + ) + + genkit.DefineFlow(g, "weather-tool", func(ctx context.Context, input any) (string, error) { + // Define the Ollama model + model := ollamaPlugin.DefineModel(g, + ollama.ModelDefinition{ + Name: "llama3.1", // Choose an appropriate model + Type: "chat", // Must be chat for tool support + }, + nil) + + // Create system message + systemMsg := ai.NewSystemTextMessage( + "You are a helpful assistant that can look up weather. " + + "When providing weather information, use the appropriate tool.") + + // Create user message + userMsg := ai.NewUserTextMessage("I'd like to know the weather in Tokyo.") + + resp, err := genkit.Generate(ctx, g, + ai.WithModel(model), + ai.WithMessages(systemMsg, userMsg), + ai.WithTools(weatherTool), + ai.WithToolChoice(ai.ToolChoiceAuto), + ) + if err != nil { + return "", err + } + + return resp.Text(), nil + }) + + genkit.DefineFlow(g, "vision", func(ctx context.Context, input any) (string, error) { + // Define a model that supports images (llava is one of the supported models) + model := ollamaPlugin.DefineModel(g, ollama.ModelDefinition{ + Name: "llava", + Type: "generate", + }, nil) + + imgData, err := readImage("test.png") + if err != nil { + return "", err + } + request := &ai.ModelRequest{ + Messages: []*ai.Message{ + { + Role: ai.RoleUser, + Content: []*ai.Part{ + ai.NewTextPart("Describe what you see in this image:"), + ai.NewMediaPart(imgData.contentType, imgData.encodedData), + }, + }, + }, + } + + resp, err := model.Generate(ctx, request, nil) + if err != nil { + return "", fmt.Errorf("error generating response: %w", err) + } + + return resp.Text(), nil + }) + + <-ctx.Done() +} + +// Helper functions + +// simulateWeather returns simulated weather data for a location +func simulateWeather(location string) WeatherData { + // In a real app, this would call a weather API + // For demonstration, we'll return mock data + tempC := 22.5 + switch location { + case "Tokyo", "Tokyo, Japan": + tempC = 24.0 + case "Paris", "Paris, France": + tempC = 18.5 + case "New York", "New York, USA": + tempC = 15.0 + } + + conditions := []string{"Sunny", "Partly Cloudy", "Cloudy", "Rainy", "Stormy"} + condition := conditions[time.Now().Unix()%int64(len(conditions))] + + return WeatherData{ + Location: location, + TempC: tempC, + TempF: tempC*9/5 + 32, + Condition: condition, + } +} + +type imageData struct { + contentType string + encodedData string +} + +// reads an image and encodes its contents as a base64 string +func readImage(path string) (*imageData, error) { + if _, err := os.Stat(path); os.IsNotExist(err) { + return nil, fmt.Errorf("image not found: %w", err) + } + + img, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("failed to read image file: %w", err) + } + + contentType := http.DetectContentType(img) + if contentType == "application/octet-stream" { + contentType = getContentTypeFromExtension(path) + } + + base64image := base64.StdEncoding.EncodeToString(img) + dataURI := fmt.Sprintf("data:%s;base64,%s", contentType, base64image) + + return &imageData{ + contentType: contentType, + encodedData: dataURI, + }, nil +} + +// getContentTypeFromExtension returns a MIME type based on file extension +func getContentTypeFromExtension(filename string) string { + ext := strings.ToLower(filepath.Ext(filename)) + switch ext { + case ".jpg", ".jpeg": + return "image/jpeg" + case ".png": + return "image/png" + case ".gif": + return "image/gif" + case ".webp": + return "image/webp" + case ".bmp": + return "image/bmp" + case ".svg": + return "image/svg+xml" + default: + return "image/png" // Default fallback + } +} diff --git a/go/samples/ollama/test.png b/go/samples/ollama/test.png new file mode 100644 index 0000000000..7636a6149b Binary files /dev/null and b/go/samples/ollama/test.png differ diff --git a/go/samples/prompts-dir/main.go b/go/samples/prompts-dir/main.go deleted file mode 100644 index 59e5e83843..0000000000 --- a/go/samples/prompts-dir/main.go +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright 2025 Google LLC -// SPDX-License-Identifier: Apache-2.0 - -// [START main] -package main - -import ( - "context" - "errors" - - // Import Genkit and the Google AI plugin - "github.com/firebase/genkit/go/ai" - "github.com/firebase/genkit/go/genkit" - "github.com/firebase/genkit/go/plugins/googlegenai" -) - -func main() { - ctx := context.Background() - - g := genkit.Init(ctx, - genkit.WithPlugins(&googlegenai.GoogleAI{}), - genkit.WithPromptDir("prompts"), - ) - - type greetingStyle struct { - Style string `json:"style"` - Location string `json:"location"` - Name string `json:"name"` - } - - type greeting struct { - Greeting string `json:"greeting"` - } - - // Define a simple flow that prompts an LLM to generate greetings using a - // given style. - genkit.DefineFlow(g, "assistantGreetingFlow", func(ctx context.Context, input greetingStyle) (string, error) { - // Look up the prompt by name - prompt := genkit.LookupPrompt(g, "example") - if prompt == nil { - return "", errors.New("assistantGreetingFlow: failed to find prompt") - } - - // Execute the prompt with the provided input - resp, err := prompt.Execute(ctx, ai.WithInput(input)) - if err != nil { - return "", err - } - - var output greeting - if err = resp.Output(&output); err != nil { - return "", err - } - - return output.Greeting, nil - }) - - <-ctx.Done() -} - -// [END main] diff --git a/go/samples/telemetry-test/README.md b/go/samples/telemetry/README.md similarity index 100% rename from go/samples/telemetry-test/README.md rename to go/samples/telemetry/README.md diff --git a/go/samples/telemetry-test/kitchen_sink/go.mod b/go/samples/telemetry/kitchen_sink/go.mod similarity index 100% rename from go/samples/telemetry-test/kitchen_sink/go.mod rename to go/samples/telemetry/kitchen_sink/go.mod diff --git a/go/samples/telemetry-test/kitchen_sink/go.sum b/go/samples/telemetry/kitchen_sink/go.sum similarity index 100% rename from go/samples/telemetry-test/kitchen_sink/go.sum rename to go/samples/telemetry/kitchen_sink/go.sum diff --git a/go/samples/telemetry-test/kitchen_sink/kitchen_sink.go b/go/samples/telemetry/kitchen_sink/kitchen_sink.go similarity index 100% rename from go/samples/telemetry-test/kitchen_sink/kitchen_sink.go rename to go/samples/telemetry/kitchen_sink/kitchen_sink.go diff --git a/go/samples/telemetry-test/simple/go.mod b/go/samples/telemetry/simple/go.mod similarity index 100% rename from go/samples/telemetry-test/simple/go.mod rename to go/samples/telemetry/simple/go.mod diff --git a/go/samples/telemetry-test/simple/go.sum b/go/samples/telemetry/simple/go.sum similarity index 100% rename from go/samples/telemetry-test/simple/go.sum rename to go/samples/telemetry/simple/go.sum diff --git a/go/samples/telemetry-test/simple/simple_joke.go b/go/samples/telemetry/simple/simple_joke.go similarity index 100% rename from go/samples/telemetry-test/simple/simple_joke.go rename to go/samples/telemetry/simple/simple_joke.go diff --git a/go/samples/text-to-speech/main.go b/go/samples/text-to-speech/main.go deleted file mode 100644 index e773f07838..0000000000 --- a/go/samples/text-to-speech/main.go +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright 2025 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 main - -import ( - "context" - "encoding/base64" - "io" - "os" - - "github.com/firebase/genkit/go/ai" - "github.com/firebase/genkit/go/genkit" - "github.com/firebase/genkit/go/plugins/googlegenai" - "google.golang.org/genai" -) - -func main() { - ctx := context.Background() - - // Initialize Genkit with the Google AI plugin. When you pass nil for the - // Config parameter, the Google AI plugin will get the API key from the - // GEMINI_API_KEY or GOOGLE_API_KEY environment variable, which is the recommended - // practice. - g := genkit.Init(ctx, - genkit.WithPlugins(&googlegenai.GoogleAI{}), - genkit.WithDefaultModel("googleai/gemini-2.5-flash-preview-tts"), - ) - - // Define a simple flow that generates an audio from a given text - genkit.DefineFlow(g, "text-to-speech-flow", func(ctx context.Context, input any) (string, error) { - resp, err := genkit.Generate(ctx, g, - ai.WithConfig(&genai.GenerateContentConfig{ - Temperature: genai.Ptr[float32](1.0), - ResponseModalities: []string{"AUDIO"}, - SpeechConfig: &genai.SpeechConfig{ - VoiceConfig: &genai.VoiceConfig{ - PrebuiltVoiceConfig: &genai.PrebuiltVoiceConfig{ - VoiceName: "Algenib", - }, - }, - }, - }), - ai.WithPrompt("Say: Genkit is the best Gen AI library!")) - if err != nil { - return "", err - } - - // base64 encoded audio - text := resp.Text() - return text, nil - }) - - // Define a simple flow that generates audio transcripts from a given audio - genkit.DefineFlow(g, "speech-to-text-flow", func(ctx context.Context, input any) (string, error) { - audio, err := os.Open("./genkit.wav") - if err != nil { - return "", err - } - defer audio.Close() - - audioBytes, err := io.ReadAll(audio) - if err != nil { - return "", err - } - resp, err := genkit.Generate(ctx, g, - ai.WithModelName("googleai/gemini-2.5-flash"), - ai.WithMessages(ai.NewUserMessage( - ai.NewTextPart("Can you transcribe the next audio?"), - ai.NewMediaPart("audio/wav", "data:audio/wav;base64,"+base64.StdEncoding.EncodeToString(audioBytes)))), - ) - if err != nil { - return "", err - } - - return resp.Text(), nil - }) - - <-ctx.Done() -}