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)
|
sub(b, 32)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func BenchmarkPub1(b *testing.B) {
|
func BenchmarkPub1(b *testing.B) {
|
||||||
pub(b, 1)
|
pub(b, 1)
|
||||||
}
|
}
|
||||||
|
@ -45,8 +45,6 @@ type publication struct {
|
|||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
func (p *publication) Ack() error {
|
func (p *publication) Ack() error {
|
||||||
return p.d.Ack(false)
|
return p.d.Ack(false)
|
||||||
}
|
}
|
||||||
|
57
cmd/cmd.go
57
cmd/cmd.go
@ -4,17 +4,12 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"os"
|
|
||||||
"sort"
|
"sort"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/urfave/cli/v2"
|
"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"
|
||||||
"go-micro.dev/v5/cache/redis"
|
"go-micro.dev/v5/cache/redis"
|
||||||
"go-micro.dev/v5/client"
|
"go-micro.dev/v5/client"
|
||||||
@ -26,6 +21,13 @@ import (
|
|||||||
"go-micro.dev/v5/events"
|
"go-micro.dev/v5/events"
|
||||||
"go-micro.dev/v5/logger"
|
"go-micro.dev/v5/logger"
|
||||||
mprofile "go-micro.dev/v5/profile"
|
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"
|
||||||
"go-micro.dev/v5/registry/consul"
|
"go-micro.dev/v5/registry/consul"
|
||||||
"go-micro.dev/v5/registry/etcd"
|
"go-micro.dev/v5/registry/etcd"
|
||||||
@ -246,6 +248,21 @@ var (
|
|||||||
EnvVars: []string{"MICRO_CONFIG"},
|
EnvVars: []string{"MICRO_CONFIG"},
|
||||||
Usage: "The source of the config to be used to get configuration",
|
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{
|
DefaultBrokers = map[string]func(...broker.Option) broker.Broker{
|
||||||
@ -295,6 +312,11 @@ var (
|
|||||||
"redis": redis.NewRedisCache,
|
"redis": redis.NewRedisCache,
|
||||||
}
|
}
|
||||||
DefaultStreams = map[string]func(...events.Option) (events.Stream, error){}
|
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() {
|
func init() {
|
||||||
@ -367,6 +389,8 @@ func (c *cmd) Options() Options {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *cmd) Before(ctx *cli.Context) error {
|
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
|
// If flags are set then use them otherwise do nothing
|
||||||
var serverOpts []server.Option
|
var serverOpts []server.Option
|
||||||
var clientOpts []client.Option
|
var clientOpts []client.Option
|
||||||
@ -799,3 +823,24 @@ func Register(cmds ...*cli.Command) {
|
|||||||
return app.Commands[i].Name < app.Commands[j].Name
|
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"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go-micro.dev/v5/events/natsjs"
|
|
||||||
nserver "github.com/nats-io/nats-server/v2/server"
|
nserver "github.com/nats-io/nats-server/v2/server"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/test-go/testify/require"
|
"github.com/test-go/testify/require"
|
||||||
"go-micro.dev/v5/events"
|
"go-micro.dev/v5/events"
|
||||||
|
"go-micro.dev/v5/events/natsjs"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Payload struct {
|
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/crypto v0.37.0
|
||||||
golang.org/x/net v0.38.0
|
golang.org/x/net v0.38.0
|
||||||
golang.org/x/sync v0.13.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 v1.71.1
|
||||||
google.golang.org/grpc/examples v0.0.0-20250515150734-f2d3e11f3057
|
google.golang.org/grpc/examples v0.0.0-20250515150734-f2d3e11f3057
|
||||||
google.golang.org/protobuf v1.36.6
|
google.golang.org/protobuf v1.36.6
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
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
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
github.com/armon/go-metrics v0.4.1 // indirect
|
github.com/armon/go-metrics v0.4.1 // indirect
|
||||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // 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/cpuguy83/go-md2man/v2 v2.0.5 // indirect
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/fatih/color v1.16.0 // 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/logr v1.4.2 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/gogo/protobuf v1.3.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/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/errwrap v1.1.0 // indirect
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||||
github.com/hashicorp/go-hclog v1.5.0 // 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
|
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
||||||
go.etcd.io/etcd/client/pkg/v3 v3.5.21 // indirect
|
go.etcd.io/etcd/client/pkg/v3 v3.5.21 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 // 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.opentelemetry.io/otel/metric v1.35.0 // indirect
|
||||||
go.uber.org/multierr v1.10.0 // indirect
|
go.uber.org/multierr v1.10.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // 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 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
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.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 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
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 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
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/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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
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 h1:0+osr/3t/aZNAdJX558crU3PEjVrG4x6715aZHRgceE=
|
||||||
github.com/hashicorp/consul/api v1.32.1/go.mod h1:mXUWLnxftwTmDv4W3lzxYCPD199iNLLUyLfLGFJbtl4=
|
github.com/hashicorp/consul/api v1.32.1/go.mod h1:mXUWLnxftwTmDv4W3lzxYCPD199iNLLUyLfLGFJbtl4=
|
||||||
github.com/hashicorp/consul/sdk v0.16.1 h1:V8TxTnImoPD5cj0U9Spl0TUxcytjcbbJeADFF07KdHg=
|
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.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 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
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 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
||||||
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
||||||
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
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-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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/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 h1:hE3bRWtU6uceqlh4fhrSnUyjKHMKB9KrTLLG+bc0ddM=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8=
|
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=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g=
|
||||||
|
@ -43,6 +43,7 @@ type natsStore struct {
|
|||||||
js nats.JetStreamContext
|
js nats.JetStreamContext
|
||||||
buckets *hashmap.Map[string, nats.KeyValue]
|
buckets *hashmap.Map[string, nats.KeyValue]
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewStore will create a new NATS JetStream Object Store.
|
// NewStore will create a new NATS JetStream Object Store.
|
||||||
func NewStore(opts ...store.Option) store.Store {
|
func NewStore(opts ...store.Option) store.Store {
|
||||||
options := store.Options{
|
options := store.Options{
|
||||||
|
@ -198,11 +198,11 @@ func (s *sqlStore) List(opts ...store.ListOption) ([]string, error) {
|
|||||||
|
|
||||||
var rows pgx.Rows
|
var rows pgx.Rows
|
||||||
if options.Limit > 0 {
|
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 {
|
} else {
|
||||||
|
|
||||||
rows, err = db.Query(s.options.Context, queries.ListAsc, pattern)
|
rows, err = db.Query(s.options.Context, queries.ListAsc, pattern)
|
||||||
|
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -273,9 +273,7 @@ func (s *sqlStore) rowsToRecords(rows pgx.Rows) ([]*store.Record, error) {
|
|||||||
|
|
||||||
// Read a single key
|
// Read a single key
|
||||||
func (s *sqlStore) Read(key string, opts ...store.ReadOption) ([]*store.Record, error) {
|
func (s *sqlStore) Read(key string, opts ...store.ReadOption) ([]*store.Record, error) {
|
||||||
options := store.ReadOptions{
|
options := store.ReadOptions{}
|
||||||
|
|
||||||
}
|
|
||||||
for _, o := range opts {
|
for _, o := range opts {
|
||||||
o(&options)
|
o(&options)
|
||||||
}
|
}
|
||||||
@ -307,11 +305,11 @@ func (s *sqlStore) Read(key string, opts ...store.ReadOption) ([]*store.Record,
|
|||||||
var rows pgx.Rows
|
var rows pgx.Rows
|
||||||
if options.Limit > 0 {
|
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 {
|
} else {
|
||||||
|
|
||||||
rows, err = db.Query(s.options.Context, queries.ListAsc, pattern)
|
rows, err = db.Query(s.options.Context, queries.ListAsc, pattern)
|
||||||
|
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -257,8 +257,6 @@ func (s *sqlStore) prepare(database, table, query string) (*sql.Stmt, error) {
|
|||||||
return nil, errors.New("unsupported statement")
|
return nil, errors.New("unsupported statement")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// get DB
|
// get DB
|
||||||
database, table = s.getDB(database, table)
|
database, table = s.getDB(database, table)
|
||||||
|
|
||||||
|
@ -61,8 +61,6 @@ var (
|
|||||||
DefaultTimeout = time.Minute
|
DefaultTimeout = time.Minute
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
func configure(n *ntport, opts ...transport.Option) {
|
func configure(n *ntport, opts ...transport.Option) {
|
||||||
for _, o := range opts {
|
for _, o := range opts {
|
||||||
o(&n.opts)
|
o(&n.opts)
|
||||||
|
Reference in New Issue
Block a user