mirror of
https://github.com/go-micro/go-micro.git
synced 2025-06-30 22:33:49 +02:00
GenAI interface (#2790)
* genai interface * x * x * text to speech * Re-add events package (#2761) * Re-add events package * run redis as a dep * remove redis events * fix: data race on event subscriber * fix: data race in tests * fix: store errors * fix: lint issues * feat: default stream * Update file.go --------- Co-authored-by: Brian Ketelsen <bketelsen@gmail.com> * . * copilot couldn't make it compile so I did * copilot couldn't make it compile so I did * x --------- Co-authored-by: Brian Ketelsen <bketelsen@gmail.com>
This commit is contained in:
@ -362,7 +362,6 @@ func BenchmarkSub32(b *testing.B) {
|
||||
sub(b, 32)
|
||||
}
|
||||
|
||||
|
||||
func BenchmarkPub1(b *testing.B) {
|
||||
pub(b, 1)
|
||||
}
|
||||
|
@ -45,8 +45,6 @@ type publication struct {
|
||||
err error
|
||||
}
|
||||
|
||||
|
||||
|
||||
func (p *publication) Ack() error {
|
||||
return p.d.Ack(false)
|
||||
}
|
||||
|
57
cmd/cmd.go
57
cmd/cmd.go
@ -4,17 +4,12 @@ package cmd
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"sort"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
"go-micro.dev/v5/auth"
|
||||
nbroker "go-micro.dev/v5/broker/nats"
|
||||
rabbit "go-micro.dev/v5/broker/rabbitmq"
|
||||
|
||||
"go-micro.dev/v5/broker"
|
||||
"go-micro.dev/v5/cache"
|
||||
"go-micro.dev/v5/cache/redis"
|
||||
"go-micro.dev/v5/client"
|
||||
@ -26,6 +21,13 @@ import (
|
||||
"go-micro.dev/v5/events"
|
||||
"go-micro.dev/v5/logger"
|
||||
mprofile "go-micro.dev/v5/profile"
|
||||
"go-micro.dev/v5/auth"
|
||||
"go-micro.dev/v5/broker"
|
||||
nbroker "go-micro.dev/v5/broker/nats"
|
||||
rabbit "go-micro.dev/v5/broker/rabbitmq"
|
||||
"go-micro.dev/v5/genai"
|
||||
"go-micro.dev/v5/genai/gemini"
|
||||
"go-micro.dev/v5/genai/openai"
|
||||
"go-micro.dev/v5/registry"
|
||||
"go-micro.dev/v5/registry/consul"
|
||||
"go-micro.dev/v5/registry/etcd"
|
||||
@ -246,6 +248,21 @@ var (
|
||||
EnvVars: []string{"MICRO_CONFIG"},
|
||||
Usage: "The source of the config to be used to get configuration",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "genai",
|
||||
EnvVars: []string{"MICRO_GENAI"},
|
||||
Usage: "GenAI provider to use (e.g. openai, gemini, noop)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "genai_key",
|
||||
EnvVars: []string{"MICRO_GENAI_KEY"},
|
||||
Usage: "GenAI API key",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "genai_model",
|
||||
EnvVars: []string{"MICRO_GENAI_MODEL"},
|
||||
Usage: "GenAI model to use (optional)",
|
||||
},
|
||||
}
|
||||
|
||||
DefaultBrokers = map[string]func(...broker.Option) broker.Broker{
|
||||
@ -295,6 +312,11 @@ var (
|
||||
"redis": redis.NewRedisCache,
|
||||
}
|
||||
DefaultStreams = map[string]func(...events.Option) (events.Stream, error){}
|
||||
|
||||
DefaultGenAI = map[string]func(...genai.Option) genai.GenAI{
|
||||
"openai": openai.New,
|
||||
"gemini": gemini.New,
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -367,6 +389,8 @@ func (c *cmd) Options() Options {
|
||||
}
|
||||
|
||||
func (c *cmd) Before(ctx *cli.Context) error {
|
||||
// Set GenAI provider from flags/env
|
||||
setGenAIFromFlags(ctx)
|
||||
// If flags are set then use them otherwise do nothing
|
||||
var serverOpts []server.Option
|
||||
var clientOpts []client.Option
|
||||
@ -799,3 +823,24 @@ func Register(cmds ...*cli.Command) {
|
||||
return app.Commands[i].Name < app.Commands[j].Name
|
||||
})
|
||||
}
|
||||
|
||||
func setGenAIFromFlags(ctx *cli.Context) {
|
||||
provider := ctx.String("genai")
|
||||
key := ctx.String("genai_key")
|
||||
model := ctx.String("genai_model")
|
||||
|
||||
switch provider {
|
||||
case "openai":
|
||||
if key == "" {
|
||||
key = os.Getenv("OPENAI_API_KEY")
|
||||
}
|
||||
genai.DefaultGenAI = openai.New(genai.WithAPIKey(key), genai.WithModel(model))
|
||||
case "gemini":
|
||||
if key == "" {
|
||||
key = os.Getenv("GEMINI_API_KEY")
|
||||
}
|
||||
genai.DefaultGenAI = gemini.New(genai.WithAPIKey(key), genai.WithModel(model))
|
||||
default:
|
||||
genai.DefaultGenAI = genai.Default
|
||||
}
|
||||
}
|
||||
|
@ -8,11 +8,11 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go-micro.dev/v5/events/natsjs"
|
||||
nserver "github.com/nats-io/nats-server/v2/server"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/test-go/testify/require"
|
||||
"go-micro.dev/v5/events"
|
||||
"go-micro.dev/v5/events/natsjs"
|
||||
)
|
||||
|
||||
type Payload struct {
|
||||
|
16
genai/default.go
Normal file
16
genai/default.go
Normal file
@ -0,0 +1,16 @@
|
||||
package genai
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
DefaultGenAI GenAI = &noopGenAI{}
|
||||
defaultOnce sync.Once
|
||||
)
|
||||
|
||||
func SetDefault(g GenAI) {
|
||||
defaultOnce.Do(func() {
|
||||
DefaultGenAI = g
|
||||
})
|
||||
}
|
161
genai/gemini/gemini.go
Normal file
161
genai/gemini/gemini.go
Normal file
@ -0,0 +1,161 @@
|
||||
package gemini
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"go-micro.dev/v5/genai"
|
||||
)
|
||||
|
||||
// gemini implements the GenAI interface using Google Gemini 2.5 API.
|
||||
type gemini struct {
|
||||
options genai.Options
|
||||
}
|
||||
|
||||
func New(opts ...genai.Option) genai.GenAI {
|
||||
var options genai.Options
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
if options.APIKey == "" {
|
||||
options.APIKey = os.Getenv("GEMINI_API_KEY")
|
||||
}
|
||||
return &gemini{options: options}
|
||||
}
|
||||
|
||||
func (g *gemini) Generate(prompt string, opts ...genai.Option) (*genai.Result, error) {
|
||||
options := g.options
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
||||
res := &genai.Result{Prompt: prompt, Type: options.Type}
|
||||
|
||||
endpoint := options.Endpoint
|
||||
if endpoint == "" {
|
||||
endpoint = "https://generativelanguage.googleapis.com/v1beta/models/"
|
||||
}
|
||||
|
||||
var url string
|
||||
var body map[string]interface{}
|
||||
|
||||
// Determine model to use
|
||||
var model string
|
||||
switch options.Type {
|
||||
case "image":
|
||||
if options.Model != "" {
|
||||
model = options.Model
|
||||
} else {
|
||||
model = "gemini-2.5-pro-vision"
|
||||
}
|
||||
url = endpoint + model + ":generateContent?key=" + options.APIKey
|
||||
body = map[string]interface{}{
|
||||
"contents": []map[string]interface{}{
|
||||
{"parts": []map[string]string{{"text": prompt}}},
|
||||
},
|
||||
}
|
||||
case "audio":
|
||||
if options.Model != "" {
|
||||
model = options.Model
|
||||
} else {
|
||||
model = "gemini-2.5-pro"
|
||||
}
|
||||
url = endpoint + model + ":generateContent?key=" + options.APIKey
|
||||
body = map[string]interface{}{
|
||||
"contents": []map[string]interface{}{
|
||||
{"parts": []map[string]string{{"text": prompt}}},
|
||||
},
|
||||
"response_mime_type": "audio/wav",
|
||||
}
|
||||
case "text":
|
||||
fallthrough
|
||||
default:
|
||||
if options.Model != "" {
|
||||
model = options.Model
|
||||
} else {
|
||||
model = "gemini-2.5-pro"
|
||||
}
|
||||
url = endpoint + model + ":generateContent?key=" + options.APIKey
|
||||
body = map[string]interface{}{
|
||||
"contents": []map[string]interface{}{
|
||||
{"parts": []map[string]string{{"text": prompt}}},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
b, _ := json.Marshal(body)
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if options.Type == "audio" {
|
||||
var result struct {
|
||||
Candidates []struct {
|
||||
Content struct {
|
||||
Parts []struct {
|
||||
InlineData struct {
|
||||
Data []byte `json:"data"`
|
||||
} `json:"inline_data"`
|
||||
} `json:"parts"`
|
||||
} `json:"content"`
|
||||
} `json:"candidates"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(result.Candidates) == 0 || len(result.Candidates[0].Content.Parts) == 0 {
|
||||
return nil, fmt.Errorf("no audio returned")
|
||||
}
|
||||
res.Data = result.Candidates[0].Content.Parts[0].InlineData.Data
|
||||
return res, nil
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Candidates []struct {
|
||||
Content struct {
|
||||
Parts []struct {
|
||||
Text string `json:"text"`
|
||||
} `json:"parts"`
|
||||
} `json:"content"`
|
||||
} `json:"candidates"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(result.Candidates) == 0 || len(result.Candidates[0].Content.Parts) == 0 {
|
||||
return nil, fmt.Errorf("no candidates returned")
|
||||
}
|
||||
res.Text = result.Candidates[0].Content.Parts[0].Text
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (g *gemini) Stream(prompt string, opts ...genai.Option) (*genai.Stream, error) {
|
||||
results := make(chan *genai.Result)
|
||||
go func() {
|
||||
defer close(results)
|
||||
res, err := g.Generate(prompt, opts...)
|
||||
if err != nil {
|
||||
// Send error via Stream.Err, not channel
|
||||
return
|
||||
}
|
||||
results <- res
|
||||
}()
|
||||
return &genai.Stream{Results: results}, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
genai.Register("gemini", New())
|
||||
}
|
53
genai/genai.go
Normal file
53
genai/genai.go
Normal file
@ -0,0 +1,53 @@
|
||||
// Package genai provides a generic interface for generative AI providers.
|
||||
package genai
|
||||
|
||||
// Result is the unified response from GenAI providers.
|
||||
type Result struct {
|
||||
Prompt string
|
||||
Type string
|
||||
Data []byte // for audio/image binary data
|
||||
Text string // for text or image URL
|
||||
}
|
||||
|
||||
// Stream represents a streaming response from a GenAI provider.
|
||||
type Stream struct {
|
||||
Results <-chan *Result
|
||||
Err error
|
||||
// You can add fields for cancellation, errors, etc. if needed
|
||||
}
|
||||
|
||||
// GenAI is the generic interface for generative AI providers.
|
||||
type GenAI interface {
|
||||
Generate(prompt string, opts ...Option) (*Result, error)
|
||||
Stream(prompt string, opts ...Option) (*Stream, error)
|
||||
}
|
||||
|
||||
// Option is a functional option for configuring providers.
|
||||
type Option func(*Options)
|
||||
|
||||
// Options holds configuration for providers.
|
||||
type Options struct {
|
||||
APIKey string
|
||||
Endpoint string
|
||||
Type string // "text", "image", "audio", etc.
|
||||
Model string // model name, e.g. "gemini-2.5-pro"
|
||||
// Add more fields as needed
|
||||
}
|
||||
|
||||
// Option functions for generation type
|
||||
func Text(o *Options) { o.Type = "text" }
|
||||
func Image(o *Options) { o.Type = "image" }
|
||||
func Audio(o *Options) { o.Type = "audio" }
|
||||
|
||||
// Provider registry
|
||||
var providers = make(map[string]GenAI)
|
||||
|
||||
// Register a GenAI provider by name.
|
||||
func Register(name string, provider GenAI) {
|
||||
providers[name] = provider
|
||||
}
|
||||
|
||||
// Get a GenAI provider by name.
|
||||
func Get(name string) GenAI {
|
||||
return providers[name]
|
||||
}
|
16
genai/noop.go
Normal file
16
genai/noop.go
Normal file
@ -0,0 +1,16 @@
|
||||
package genai
|
||||
|
||||
type noopGenAI struct{}
|
||||
|
||||
func (n *noopGenAI) Generate(prompt string, opts ...Option) (*Result, error) {
|
||||
return &Result{Prompt: prompt, Type: "noop", Text: "noop response"}, nil
|
||||
}
|
||||
|
||||
func (n *noopGenAI) Stream(prompt string, opts ...Option) (*Stream, error) {
|
||||
results := make(chan *Result, 1)
|
||||
results <- &Result{Prompt: prompt, Type: "noop", Text: "noop response"}
|
||||
close(results)
|
||||
return &Stream{Results: results}, nil
|
||||
}
|
||||
|
||||
var Default = &noopGenAI{}
|
151
genai/openai/openai.go
Normal file
151
genai/openai/openai.go
Normal file
@ -0,0 +1,151 @@
|
||||
package openai
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"go-micro.dev/v5/genai"
|
||||
)
|
||||
|
||||
type openAI struct {
|
||||
options genai.Options
|
||||
}
|
||||
|
||||
func New(opts ...genai.Option) genai.GenAI {
|
||||
var options genai.Options
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
if options.APIKey == "" {
|
||||
options.APIKey = os.Getenv("OPENAI_API_KEY")
|
||||
}
|
||||
return &openAI{options: options}
|
||||
}
|
||||
|
||||
func (o *openAI) Generate(prompt string, opts ...genai.Option) (*genai.Result, error) {
|
||||
options := o.options
|
||||
for _, opt := range opts {
|
||||
opt(&options)
|
||||
}
|
||||
|
||||
res := &genai.Result{Prompt: prompt, Type: options.Type}
|
||||
|
||||
var url string
|
||||
var body map[string]interface{}
|
||||
|
||||
switch options.Type {
|
||||
case "image":
|
||||
model := options.Model
|
||||
if model == "" {
|
||||
model = "dall-e-3"
|
||||
}
|
||||
url = "https://api.openai.com/v1/images/generations"
|
||||
body = map[string]interface{}{
|
||||
"prompt": prompt,
|
||||
"n": 1,
|
||||
"size": "1024x1024",
|
||||
"model": model,
|
||||
}
|
||||
case "audio":
|
||||
model := options.Model
|
||||
if model == "" {
|
||||
model = "tts-1"
|
||||
}
|
||||
url = "https://api.openai.com/v1/audio/speech"
|
||||
body = map[string]interface{}{
|
||||
"model": model,
|
||||
"input": prompt,
|
||||
"voice": "alloy", // or another supported voice
|
||||
}
|
||||
case "text":
|
||||
fallthrough
|
||||
default:
|
||||
model := options.Model
|
||||
if model == "" {
|
||||
model = "gpt-3.5-turbo"
|
||||
}
|
||||
url = "https://api.openai.com/v1/chat/completions"
|
||||
body = map[string]interface{}{
|
||||
"model": model,
|
||||
"messages": []map[string]string{{"role": "user", "content": prompt}},
|
||||
}
|
||||
}
|
||||
|
||||
b, _ := json.Marshal(body)
|
||||
req, err := http.NewRequest("POST", url, bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+options.APIKey)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
switch options.Type {
|
||||
case "image":
|
||||
var result struct {
|
||||
Data []struct {
|
||||
URL string `json:"url"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(result.Data) == 0 {
|
||||
return nil, fmt.Errorf("no image returned")
|
||||
}
|
||||
res.Text = result.Data[0].URL
|
||||
return res, nil
|
||||
case "audio":
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res.Data = data
|
||||
return res, nil
|
||||
case "text":
|
||||
fallthrough
|
||||
default:
|
||||
var result struct {
|
||||
Choices []struct {
|
||||
Message struct {
|
||||
Content string `json:"content"`
|
||||
} `json:"message"`
|
||||
} `json:"choices"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(result.Choices) == 0 {
|
||||
return nil, fmt.Errorf("no choices returned")
|
||||
}
|
||||
res.Text = result.Choices[0].Message.Content
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (o *openAI) Stream(prompt string, opts ...genai.Option) (*genai.Stream, error) {
|
||||
results := make(chan *genai.Result)
|
||||
go func() {
|
||||
defer close(results)
|
||||
res, err := o.Generate(prompt, opts...)
|
||||
if err != nil {
|
||||
// Send error via Stream.Err, not channel
|
||||
return
|
||||
}
|
||||
results <- res
|
||||
}()
|
||||
return &genai.Stream{Results: results}, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
genai.Register("openai", New())
|
||||
}
|
37
genai/openai/openai_test.go
Normal file
37
genai/openai/openai_test.go
Normal file
@ -0,0 +1,37 @@
|
||||
package openai
|
||||
|
||||
import (
|
||||
"go-micro.dev/v5/genai"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestOpenAI_GenerateText(t *testing.T) {
|
||||
apiKey := os.Getenv("OPENAI_API_KEY")
|
||||
if apiKey == "" {
|
||||
t.Skip("OPENAI_API_KEY not set")
|
||||
}
|
||||
client := New(genai.WithAPIKey(apiKey))
|
||||
res, err := client.Generate("Say hello world", genai.Text)
|
||||
if err != nil {
|
||||
t.Fatalf("Generate error: %v", err)
|
||||
}
|
||||
if res == nil || res.Text == "" {
|
||||
t.Error("Expected non-empty text response")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenAI_GenerateImage(t *testing.T) {
|
||||
apiKey := os.Getenv("OPENAI_API_KEY")
|
||||
if apiKey == "" {
|
||||
t.Skip("OPENAI_API_KEY not set")
|
||||
}
|
||||
client := New(genai.WithAPIKey(apiKey))
|
||||
res, err := client.Generate("A cat wearing sunglasses", genai.Image)
|
||||
if err != nil {
|
||||
t.Fatalf("Generate error: %v", err)
|
||||
}
|
||||
if res == nil || res.Text == "" {
|
||||
t.Error("Expected non-empty image URL")
|
||||
}
|
||||
}
|
20
genai/options.go
Normal file
20
genai/options.go
Normal file
@ -0,0 +1,20 @@
|
||||
package genai
|
||||
|
||||
// Option sets options for a GenAI provider.
|
||||
func WithAPIKey(key string) Option {
|
||||
return func(o *Options) {
|
||||
o.APIKey = key
|
||||
}
|
||||
}
|
||||
|
||||
func WithEndpoint(endpoint string) Option {
|
||||
return func(o *Options) {
|
||||
o.Endpoint = endpoint
|
||||
}
|
||||
}
|
||||
|
||||
func WithModel(model string) Option {
|
||||
return func(o *Options) {
|
||||
o.Model = model
|
||||
}
|
||||
}
|
11
go.mod
11
go.mod
@ -40,12 +40,16 @@ require (
|
||||
golang.org/x/crypto v0.37.0
|
||||
golang.org/x/net v0.38.0
|
||||
golang.org/x/sync v0.13.0
|
||||
google.golang.org/genai v1.12.0
|
||||
google.golang.org/grpc v1.71.1
|
||||
google.golang.org/grpc/examples v0.0.0-20250515150734-f2d3e11f3057
|
||||
google.golang.org/protobuf v1.36.6
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.120.0 // indirect
|
||||
cloud.google.com/go/auth v0.15.0 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.6.0 // indirect
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/armon/go-metrics v0.4.1 // indirect
|
||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
|
||||
@ -55,10 +59,16 @@ require (
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/fatih/color v1.16.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/go-tpm v0.9.3 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-hclog v1.5.0 // indirect
|
||||
@ -91,6 +101,7 @@ require (
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.21 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
|
||||
|
20
go.sum
20
go.sum
@ -1,3 +1,9 @@
|
||||
cloud.google.com/go v0.120.0 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA=
|
||||
cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q=
|
||||
cloud.google.com/go/auth v0.15.0 h1:Ly0u4aA5vG/fsSsxu98qCQBemXtAtJf+95z9HK+cxps=
|
||||
cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQNVXLZ8=
|
||||
cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
|
||||
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
@ -55,6 +61,8 @@ github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
@ -95,8 +103,16 @@ github.com/google/go-tpm v0.9.3 h1:+yx0/anQuGzi+ssRqeD6WpXjW2L/V0dItUayO0i9sRc=
|
||||
github.com/google/go-tpm v0.9.3/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||
github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q=
|
||||
github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hashicorp/consul/api v1.32.1 h1:0+osr/3t/aZNAdJX558crU3PEjVrG4x6715aZHRgceE=
|
||||
github.com/hashicorp/consul/api v1.32.1/go.mod h1:mXUWLnxftwTmDv4W3lzxYCPD199iNLLUyLfLGFJbtl4=
|
||||
github.com/hashicorp/consul/sdk v0.16.1 h1:V8TxTnImoPD5cj0U9Spl0TUxcytjcbbJeADFF07KdHg=
|
||||
@ -355,6 +371,8 @@ go.etcd.io/etcd/client/v3 v3.5.21 h1:T6b1Ow6fNjOLOtM0xSoKNQt1ASPCLWrF9XMHcH9pEyY
|
||||
go.etcd.io/etcd/client/v3 v3.5.21/go.mod h1:mFYy67IOqmbRf/kRUvsHixzo3iG+1OF2W2+jVIQRAnU=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
|
||||
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
||||
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
||||
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
||||
@ -492,6 +510,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genai v1.12.0 h1:0JjAdwvEAha9ZpPH5hL6dVG8bpMnRbAMCgv2f2LDnz4=
|
||||
google.golang.org/genai v1.12.0/go.mod h1:HFXR1zT3LCdLxd/NW6IOSCczOYyRAxwaShvYbgPSeVw=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 h1:hE3bRWtU6uceqlh4fhrSnUyjKHMKB9KrTLLG+bc0ddM=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g=
|
||||
|
@ -43,6 +43,7 @@ type natsStore struct {
|
||||
js nats.JetStreamContext
|
||||
buckets *hashmap.Map[string, nats.KeyValue]
|
||||
}
|
||||
|
||||
// NewStore will create a new NATS JetStream Object Store.
|
||||
func NewStore(opts ...store.Option) store.Store {
|
||||
options := store.Options{
|
||||
|
@ -198,11 +198,11 @@ func (s *sqlStore) List(opts ...store.ListOption) ([]string, error) {
|
||||
|
||||
var rows pgx.Rows
|
||||
if options.Limit > 0 {
|
||||
rows, err = db.Query(s.options.Context, queries.ListAscLimit, pattern, options.Limit, options.Offset)
|
||||
rows, err = db.Query(s.options.Context, queries.ListAscLimit, pattern, options.Limit, options.Offset)
|
||||
|
||||
} else {
|
||||
|
||||
rows, err = db.Query(s.options.Context, queries.ListAsc, pattern)
|
||||
rows, err = db.Query(s.options.Context, queries.ListAsc, pattern)
|
||||
|
||||
}
|
||||
if err != nil {
|
||||
@ -273,9 +273,7 @@ func (s *sqlStore) rowsToRecords(rows pgx.Rows) ([]*store.Record, error) {
|
||||
|
||||
// Read a single key
|
||||
func (s *sqlStore) Read(key string, opts ...store.ReadOption) ([]*store.Record, error) {
|
||||
options := store.ReadOptions{
|
||||
|
||||
}
|
||||
options := store.ReadOptions{}
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
@ -307,11 +305,11 @@ func (s *sqlStore) Read(key string, opts ...store.ReadOption) ([]*store.Record,
|
||||
var rows pgx.Rows
|
||||
if options.Limit > 0 {
|
||||
|
||||
rows, err = db.Query(s.options.Context, queries.ListAscLimit, pattern, options.Limit, options.Offset)
|
||||
rows, err = db.Query(s.options.Context, queries.ListAscLimit, pattern, options.Limit, options.Offset)
|
||||
|
||||
} else {
|
||||
|
||||
rows, err = db.Query(s.options.Context, queries.ListAsc, pattern)
|
||||
rows, err = db.Query(s.options.Context, queries.ListAsc, pattern)
|
||||
|
||||
}
|
||||
if err != nil {
|
||||
|
@ -257,8 +257,6 @@ func (s *sqlStore) prepare(database, table, query string) (*sql.Stmt, error) {
|
||||
return nil, errors.New("unsupported statement")
|
||||
}
|
||||
|
||||
|
||||
|
||||
// get DB
|
||||
database, table = s.getDB(database, table)
|
||||
|
||||
|
@ -61,8 +61,6 @@ var (
|
||||
DefaultTimeout = time.Minute
|
||||
)
|
||||
|
||||
|
||||
|
||||
func configure(n *ntport, opts ...transport.Option) {
|
||||
for _, o := range opts {
|
||||
o(&n.opts)
|
||||
|
Reference in New Issue
Block a user