Skip to content

Commit ee9f3af

Browse files
asimbketelsen
andauthored
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>
1 parent 7e1bba2 commit ee9f3af

File tree

17 files changed

+543
-21
lines changed

17 files changed

+543
-21
lines changed

broker/http_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,6 @@ func BenchmarkSub32(b *testing.B) {
362362
sub(b, 32)
363363
}
364364

365-
366365
func BenchmarkPub1(b *testing.B) {
367366
pub(b, 1)
368367
}

broker/rabbitmq/rabbitmq.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,6 @@ type publication struct {
4545
err error
4646
}
4747

48-
49-
5048
func (p *publication) Ack() error {
5149
return p.d.Ack(false)
5250
}

cmd/cmd.go

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,12 @@ package cmd
44
import (
55
"fmt"
66
"math/rand"
7-
"os"
87
"sort"
8+
"os"
99
"strings"
1010
"time"
1111

1212
"github.com/urfave/cli/v2"
13-
"go-micro.dev/v5/auth"
14-
nbroker "go-micro.dev/v5/broker/nats"
15-
rabbit "go-micro.dev/v5/broker/rabbitmq"
16-
17-
"go-micro.dev/v5/broker"
1813
"go-micro.dev/v5/cache"
1914
"go-micro.dev/v5/cache/redis"
2015
"go-micro.dev/v5/client"
@@ -26,6 +21,13 @@ import (
2621
"go-micro.dev/v5/events"
2722
"go-micro.dev/v5/logger"
2823
mprofile "go-micro.dev/v5/profile"
24+
"go-micro.dev/v5/auth"
25+
"go-micro.dev/v5/broker"
26+
nbroker "go-micro.dev/v5/broker/nats"
27+
rabbit "go-micro.dev/v5/broker/rabbitmq"
28+
"go-micro.dev/v5/genai"
29+
"go-micro.dev/v5/genai/gemini"
30+
"go-micro.dev/v5/genai/openai"
2931
"go-micro.dev/v5/registry"
3032
"go-micro.dev/v5/registry/consul"
3133
"go-micro.dev/v5/registry/etcd"
@@ -246,6 +248,21 @@ var (
246248
EnvVars: []string{"MICRO_CONFIG"},
247249
Usage: "The source of the config to be used to get configuration",
248250
},
251+
&cli.StringFlag{
252+
Name: "genai",
253+
EnvVars: []string{"MICRO_GENAI"},
254+
Usage: "GenAI provider to use (e.g. openai, gemini, noop)",
255+
},
256+
&cli.StringFlag{
257+
Name: "genai_key",
258+
EnvVars: []string{"MICRO_GENAI_KEY"},
259+
Usage: "GenAI API key",
260+
},
261+
&cli.StringFlag{
262+
Name: "genai_model",
263+
EnvVars: []string{"MICRO_GENAI_MODEL"},
264+
Usage: "GenAI model to use (optional)",
265+
},
249266
}
250267

251268
DefaultBrokers = map[string]func(...broker.Option) broker.Broker{
@@ -295,6 +312,11 @@ var (
295312
"redis": redis.NewRedisCache,
296313
}
297314
DefaultStreams = map[string]func(...events.Option) (events.Stream, error){}
315+
316+
DefaultGenAI = map[string]func(...genai.Option) genai.GenAI{
317+
"openai": openai.New,
318+
"gemini": gemini.New,
319+
}
298320
)
299321

300322
func init() {
@@ -367,6 +389,8 @@ func (c *cmd) Options() Options {
367389
}
368390

369391
func (c *cmd) Before(ctx *cli.Context) error {
392+
// Set GenAI provider from flags/env
393+
setGenAIFromFlags(ctx)
370394
// If flags are set then use them otherwise do nothing
371395
var serverOpts []server.Option
372396
var clientOpts []client.Option
@@ -799,3 +823,24 @@ func Register(cmds ...*cli.Command) {
799823
return app.Commands[i].Name < app.Commands[j].Name
800824
})
801825
}
826+
827+
func setGenAIFromFlags(ctx *cli.Context) {
828+
provider := ctx.String("genai")
829+
key := ctx.String("genai_key")
830+
model := ctx.String("genai_model")
831+
832+
switch provider {
833+
case "openai":
834+
if key == "" {
835+
key = os.Getenv("OPENAI_API_KEY")
836+
}
837+
genai.DefaultGenAI = openai.New(genai.WithAPIKey(key), genai.WithModel(model))
838+
case "gemini":
839+
if key == "" {
840+
key = os.Getenv("GEMINI_API_KEY")
841+
}
842+
genai.DefaultGenAI = gemini.New(genai.WithAPIKey(key), genai.WithModel(model))
843+
default:
844+
genai.DefaultGenAI = genai.Default
845+
}
846+
}

events/natsjs/nats_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ import (
88
"testing"
99
"time"
1010

11-
"go-micro.dev/v5/events/natsjs"
1211
nserver "github.com/nats-io/nats-server/v2/server"
1312
"github.com/stretchr/testify/assert"
1413
"github.com/test-go/testify/require"
1514
"go-micro.dev/v5/events"
15+
"go-micro.dev/v5/events/natsjs"
1616
)
1717

1818
type Payload struct {

genai/default.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package genai
2+
3+
import (
4+
"sync"
5+
)
6+
7+
var (
8+
DefaultGenAI GenAI = &noopGenAI{}
9+
defaultOnce sync.Once
10+
)
11+
12+
func SetDefault(g GenAI) {
13+
defaultOnce.Do(func() {
14+
DefaultGenAI = g
15+
})
16+
}

genai/gemini/gemini.go

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
package gemini
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"fmt"
8+
"net/http"
9+
"os"
10+
11+
"go-micro.dev/v5/genai"
12+
)
13+
14+
// gemini implements the GenAI interface using Google Gemini 2.5 API.
15+
type gemini struct {
16+
options genai.Options
17+
}
18+
19+
func New(opts ...genai.Option) genai.GenAI {
20+
var options genai.Options
21+
for _, o := range opts {
22+
o(&options)
23+
}
24+
if options.APIKey == "" {
25+
options.APIKey = os.Getenv("GEMINI_API_KEY")
26+
}
27+
return &gemini{options: options}
28+
}
29+
30+
func (g *gemini) Generate(prompt string, opts ...genai.Option) (*genai.Result, error) {
31+
options := g.options
32+
for _, o := range opts {
33+
o(&options)
34+
}
35+
ctx := context.Background()
36+
37+
res := &genai.Result{Prompt: prompt, Type: options.Type}
38+
39+
endpoint := options.Endpoint
40+
if endpoint == "" {
41+
endpoint = "https://generativelanguage.googleapis.com/v1beta/models/"
42+
}
43+
44+
var url string
45+
var body map[string]interface{}
46+
47+
// Determine model to use
48+
var model string
49+
switch options.Type {
50+
case "image":
51+
if options.Model != "" {
52+
model = options.Model
53+
} else {
54+
model = "gemini-2.5-pro-vision"
55+
}
56+
url = endpoint + model + ":generateContent?key=" + options.APIKey
57+
body = map[string]interface{}{
58+
"contents": []map[string]interface{}{
59+
{"parts": []map[string]string{{"text": prompt}}},
60+
},
61+
}
62+
case "audio":
63+
if options.Model != "" {
64+
model = options.Model
65+
} else {
66+
model = "gemini-2.5-pro"
67+
}
68+
url = endpoint + model + ":generateContent?key=" + options.APIKey
69+
body = map[string]interface{}{
70+
"contents": []map[string]interface{}{
71+
{"parts": []map[string]string{{"text": prompt}}},
72+
},
73+
"response_mime_type": "audio/wav",
74+
}
75+
case "text":
76+
fallthrough
77+
default:
78+
if options.Model != "" {
79+
model = options.Model
80+
} else {
81+
model = "gemini-2.5-pro"
82+
}
83+
url = endpoint + model + ":generateContent?key=" + options.APIKey
84+
body = map[string]interface{}{
85+
"contents": []map[string]interface{}{
86+
{"parts": []map[string]string{{"text": prompt}}},
87+
},
88+
}
89+
}
90+
91+
b, _ := json.Marshal(body)
92+
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(b))
93+
if err != nil {
94+
return nil, err
95+
}
96+
req.Header.Set("Content-Type", "application/json")
97+
98+
resp, err := http.DefaultClient.Do(req)
99+
if err != nil {
100+
return nil, err
101+
}
102+
defer resp.Body.Close()
103+
104+
if options.Type == "audio" {
105+
var result struct {
106+
Candidates []struct {
107+
Content struct {
108+
Parts []struct {
109+
InlineData struct {
110+
Data []byte `json:"data"`
111+
} `json:"inline_data"`
112+
} `json:"parts"`
113+
} `json:"content"`
114+
} `json:"candidates"`
115+
}
116+
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
117+
return nil, err
118+
}
119+
if len(result.Candidates) == 0 || len(result.Candidates[0].Content.Parts) == 0 {
120+
return nil, fmt.Errorf("no audio returned")
121+
}
122+
res.Data = result.Candidates[0].Content.Parts[0].InlineData.Data
123+
return res, nil
124+
}
125+
126+
var result struct {
127+
Candidates []struct {
128+
Content struct {
129+
Parts []struct {
130+
Text string `json:"text"`
131+
} `json:"parts"`
132+
} `json:"content"`
133+
} `json:"candidates"`
134+
}
135+
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
136+
return nil, err
137+
}
138+
if len(result.Candidates) == 0 || len(result.Candidates[0].Content.Parts) == 0 {
139+
return nil, fmt.Errorf("no candidates returned")
140+
}
141+
res.Text = result.Candidates[0].Content.Parts[0].Text
142+
return res, nil
143+
}
144+
145+
func (g *gemini) Stream(prompt string, opts ...genai.Option) (*genai.Stream, error) {
146+
results := make(chan *genai.Result)
147+
go func() {
148+
defer close(results)
149+
res, err := g.Generate(prompt, opts...)
150+
if err != nil {
151+
// Send error via Stream.Err, not channel
152+
return
153+
}
154+
results <- res
155+
}()
156+
return &genai.Stream{Results: results}, nil
157+
}
158+
159+
func init() {
160+
genai.Register("gemini", New())
161+
}

genai/genai.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Package genai provides a generic interface for generative AI providers.
2+
package genai
3+
4+
// Result is the unified response from GenAI providers.
5+
type Result struct {
6+
Prompt string
7+
Type string
8+
Data []byte // for audio/image binary data
9+
Text string // for text or image URL
10+
}
11+
12+
// Stream represents a streaming response from a GenAI provider.
13+
type Stream struct {
14+
Results <-chan *Result
15+
Err error
16+
// You can add fields for cancellation, errors, etc. if needed
17+
}
18+
19+
// GenAI is the generic interface for generative AI providers.
20+
type GenAI interface {
21+
Generate(prompt string, opts ...Option) (*Result, error)
22+
Stream(prompt string, opts ...Option) (*Stream, error)
23+
}
24+
25+
// Option is a functional option for configuring providers.
26+
type Option func(*Options)
27+
28+
// Options holds configuration for providers.
29+
type Options struct {
30+
APIKey string
31+
Endpoint string
32+
Type string // "text", "image", "audio", etc.
33+
Model string // model name, e.g. "gemini-2.5-pro"
34+
// Add more fields as needed
35+
}
36+
37+
// Option functions for generation type
38+
func Text(o *Options) { o.Type = "text" }
39+
func Image(o *Options) { o.Type = "image" }
40+
func Audio(o *Options) { o.Type = "audio" }
41+
42+
// Provider registry
43+
var providers = make(map[string]GenAI)
44+
45+
// Register a GenAI provider by name.
46+
func Register(name string, provider GenAI) {
47+
providers[name] = provider
48+
}
49+
50+
// Get a GenAI provider by name.
51+
func Get(name string) GenAI {
52+
return providers[name]
53+
}

genai/noop.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package genai
2+
3+
type noopGenAI struct{}
4+
5+
func (n *noopGenAI) Generate(prompt string, opts ...Option) (*Result, error) {
6+
return &Result{Prompt: prompt, Type: "noop", Text: "noop response"}, nil
7+
}
8+
9+
func (n *noopGenAI) Stream(prompt string, opts ...Option) (*Stream, error) {
10+
results := make(chan *Result, 1)
11+
results <- &Result{Prompt: prompt, Type: "noop", Text: "noop response"}
12+
close(results)
13+
return &Stream{Results: results}, nil
14+
}
15+
16+
var Default = &noopGenAI{}

0 commit comments

Comments
 (0)