feat: integrate AI gateway for free-form queries in discord-bot
This commit is contained in:
parent
520f5d1ffb
commit
5d732405b8
@ -1,6 +1,7 @@
|
|||||||
DISCORD_TOKEN=your-bot-token-here
|
DISCORD_TOKEN=your-bot-token-here
|
||||||
GUILD_ID=your-guild-id-here
|
GUILD_ID=your-guild-id-here
|
||||||
HA_GATEWAY_ADDR=localhost:50051
|
HA_GATEWAY_ADDR=localhost:50051
|
||||||
|
AI_GATEWAY_ADDR=localhost:50052
|
||||||
OTEL_ENDPOINT=
|
OTEL_ENDPOINT=
|
||||||
LOG_LEVEL=info
|
LOG_LEVEL=info
|
||||||
LOG_FORMAT=text
|
LOG_FORMAT=text
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
|
|
||||||
discordadapter "gitea.nik4nao.com/nik/home-services/discord-bot/internal/adapters/primary/discord"
|
discordadapter "gitea.nik4nao.com/nik/home-services/discord-bot/internal/adapters/primary/discord"
|
||||||
|
"gitea.nik4nao.com/nik/home-services/discord-bot/internal/adapters/secondary/aigateway"
|
||||||
"gitea.nik4nao.com/nik/home-services/discord-bot/internal/adapters/secondary/gateway"
|
"gitea.nik4nao.com/nik/home-services/discord-bot/internal/adapters/secondary/gateway"
|
||||||
"gitea.nik4nao.com/nik/home-services/discord-bot/internal/app"
|
"gitea.nik4nao.com/nik/home-services/discord-bot/internal/app"
|
||||||
"gitea.nik4nao.com/nik/home-services/discord-bot/internal/config"
|
"gitea.nik4nao.com/nik/home-services/discord-bot/internal/config"
|
||||||
@ -38,6 +39,7 @@ func main() {
|
|||||||
log.Info("starting discord-bot",
|
log.Info("starting discord-bot",
|
||||||
"version", version,
|
"version", version,
|
||||||
"ha_gateway_addr", cfg.HAGatewayAddr,
|
"ha_gateway_addr", cfg.HAGatewayAddr,
|
||||||
|
"ai_gateway_addr", cfg.AIGatewayAddr,
|
||||||
"discord_token", redactToken(cfg.DiscordToken),
|
"discord_token", redactToken(cfg.DiscordToken),
|
||||||
"tls_dir", cfg.TLSDir,
|
"tls_dir", cfg.TLSDir,
|
||||||
"log_level", cfg.LogLevel,
|
"log_level", cfg.LogLevel,
|
||||||
@ -71,7 +73,18 @@ func main() {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
commandApp := app.NewCommandApp(haClient)
|
aiClient, err := aigateway.New(ctx, cfg.AIGatewayAddr, cfg.TLSDir, log)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("ai-gateway client setup failed", "err", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := aiClient.Close(); err != nil {
|
||||||
|
log.Error("ai-gateway client close failed", "err", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
commandApp := app.NewCommandApp(haClient, aiClient)
|
||||||
|
|
||||||
// Discord-specific wiring stays at the edge so the app layer remains transport-agnostic.
|
// Discord-specific wiring stays at the edge so the app layer remains transport-agnostic.
|
||||||
session, err := discordgo.New("Bot " + cfg.DiscordToken)
|
session, err := discordgo.New("Bot " + cfg.DiscordToken)
|
||||||
@ -99,7 +112,12 @@ func main() {
|
|||||||
if cfg.GuildID != "" {
|
if cfg.GuildID != "" {
|
||||||
scope = cfg.GuildID
|
scope = cfg.GuildID
|
||||||
}
|
}
|
||||||
log.Info("discord bot started", "command_scope", scope, "ha_gateway_addr", cfg.HAGatewayAddr, "version", version)
|
log.Info("discord bot started",
|
||||||
|
"command_scope", scope,
|
||||||
|
"ha_gateway_addr", cfg.HAGatewayAddr,
|
||||||
|
"ai_gateway_addr", cfg.AIGatewayAddr,
|
||||||
|
"version", version,
|
||||||
|
)
|
||||||
|
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
log.Info("shutdown signal received, closing session")
|
log.Info("shutdown signal received, closing session")
|
||||||
|
|||||||
@ -23,6 +23,7 @@ type commandHandler interface {
|
|||||||
HandleLightOff(ctx context.Context, entityID string, transition *uint32) (string, error)
|
HandleLightOff(ctx context.Context, entityID string, transition *uint32) (string, error)
|
||||||
HandleLightToggle(ctx context.Context, entityID string) (string, error)
|
HandleLightToggle(ctx context.Context, entityID string) (string, error)
|
||||||
HandleSwitchList(ctx context.Context) (string, error)
|
HandleSwitchList(ctx context.Context) (string, error)
|
||||||
|
HandleAIQuery(ctx context.Context, text string) (string, error)
|
||||||
AutocompleteLights(ctx context.Context) ([]apppkg.Choice, error)
|
AutocompleteLights(ctx context.Context) ([]apppkg.Choice, error)
|
||||||
AutocompleteSwitches(ctx context.Context) ([]apppkg.Choice, error)
|
AutocompleteSwitches(ctx context.Context) ([]apppkg.Choice, error)
|
||||||
}
|
}
|
||||||
@ -136,6 +137,16 @@ func (h *Handler) handleApplicationCommand(ctx context.Context, s *discordgo.Ses
|
|||||||
}
|
}
|
||||||
msg, err := h.app.HandleLightToggle(ctx, requiredStringOption(sub, "light"))
|
msg, err := h.app.HandleLightToggle(ctx, requiredStringOption(sub, "light"))
|
||||||
h.followup(ctx, s, i.Interaction, msg, true, start, err)
|
h.followup(ctx, s, i.Interaction, msg, true, start, err)
|
||||||
|
case "ai.query":
|
||||||
|
if err := h.deferResponse(s, i.Interaction, true); err != nil {
|
||||||
|
log.Error("discord response failed",
|
||||||
|
"duration_ms", time.Since(start).Milliseconds(),
|
||||||
|
"error", err.Error(),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
msg, err := h.app.HandleAIQuery(ctx, requiredStringOption(sub, "text"))
|
||||||
|
h.followup(ctx, s, i.Interaction, msg, true, start, err)
|
||||||
default:
|
default:
|
||||||
h.respondError(ctx, s, i.Interaction, true, start, fmt.Errorf("unsupported command: %s.%s", data.Name, sub.Name))
|
h.respondError(ctx, s, i.Interaction, true, start, fmt.Errorf("unsupported command: %s.%s", data.Name, sub.Name))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -81,6 +81,25 @@ func RegisterCommands(s *discordgo.Session, guildID string) error {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "ai",
|
||||||
|
Description: "Query the AI home assistant",
|
||||||
|
Options: []*discordgo.ApplicationCommandOption{
|
||||||
|
{
|
||||||
|
Type: discordgo.ApplicationCommandOptionSubCommand,
|
||||||
|
Name: "query",
|
||||||
|
Description: "Send a free-form prompt",
|
||||||
|
Options: []*discordgo.ApplicationCommandOption{
|
||||||
|
{
|
||||||
|
Type: discordgo.ApplicationCommandOptionString,
|
||||||
|
Name: "text",
|
||||||
|
Description: "Prompt text",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := s.ApplicationCommandBulkOverwrite(appID, guildID, commands); err != nil {
|
if _, err := s.ApplicationCommandBulkOverwrite(appID, guildID, commands); err != nil {
|
||||||
|
|||||||
107
discord-bot/internal/adapters/secondary/aigateway/client.go
Normal file
107
discord-bot/internal/adapters/secondary/aigateway/client.go
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
package aigateway
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.nik4nao.com/nik/home-services/discord-bot/internal/logger"
|
||||||
|
aiv1 "gitea.nik4nao.com/nik/home-services/gen/ai/v1"
|
||||||
|
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/credentials"
|
||||||
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client implements the app's AI driven port over gRPC.
|
||||||
|
type Client struct {
|
||||||
|
conn *grpc.ClientConn
|
||||||
|
client aiv1.AIServiceClient
|
||||||
|
log *slog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// New constructs a gRPC client for the internal ai-gateway service.
|
||||||
|
func New(ctx context.Context, addr, tlsDir string, log *slog.Logger) (*Client, error) {
|
||||||
|
transportCreds := insecure.NewCredentials()
|
||||||
|
if tlsDir != "" {
|
||||||
|
creds, err := loadTransportCredentials(tlsDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("load mTLS credentials: %w", err)
|
||||||
|
}
|
||||||
|
transportCreds = creds
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := grpc.NewClient(
|
||||||
|
addr,
|
||||||
|
grpc.WithTransportCredentials(transportCreds),
|
||||||
|
grpc.WithStatsHandler(otelgrpc.NewClientHandler()),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("dial ai-gateway: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Client{
|
||||||
|
conn: conn,
|
||||||
|
client: aiv1.NewAIServiceClient(conn),
|
||||||
|
log: log,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the underlying gRPC connection.
|
||||||
|
func (c *Client) Close() error {
|
||||||
|
if err := c.conn.Close(); err != nil {
|
||||||
|
return fmt.Errorf("close ai-gateway client: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query forwards one free-form request to ai-gateway.
|
||||||
|
func (c *Client) Query(ctx context.Context, text string) (string, error) {
|
||||||
|
start := time.Now()
|
||||||
|
log := logger.FromContext(ctx).With("grpc.method", "AIService/Query")
|
||||||
|
resp, err := c.client.Query(ctx, &aiv1.QueryRequest{
|
||||||
|
Text: text,
|
||||||
|
Source: "discord-bot",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Error("grpc call failed",
|
||||||
|
"duration_ms", time.Since(start).Milliseconds(),
|
||||||
|
"error", err.Error(),
|
||||||
|
)
|
||||||
|
return "", fmt.Errorf("query ai-gateway: %w", err)
|
||||||
|
}
|
||||||
|
log.Debug("grpc call completed", "duration_ms", time.Since(start).Milliseconds())
|
||||||
|
return resp.GetReply(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadTransportCredentials(tlsDir string) (credentials.TransportCredentials, error) {
|
||||||
|
cert, err := tls.LoadX509KeyPair(
|
||||||
|
filepath.Join(tlsDir, "tls.crt"),
|
||||||
|
filepath.Join(tlsDir, "tls.key"),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("load client key pair: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
caPEM, err := os.ReadFile(filepath.Join(tlsDir, "ca.crt"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read server CA: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rootCAs := x509.NewCertPool()
|
||||||
|
if !rootCAs.AppendCertsFromPEM(caPEM) {
|
||||||
|
return nil, fmt.Errorf("append server CA: invalid PEM")
|
||||||
|
}
|
||||||
|
|
||||||
|
return credentials.NewTLS(&tls.Config{
|
||||||
|
Certificates: []tls.Certificate{cert},
|
||||||
|
RootCAs: rootCAs,
|
||||||
|
ServerName: "ai-gateway.home-services.svc.cluster.local",
|
||||||
|
MinVersion: tls.VersionTLS13,
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
@ -18,11 +18,12 @@ type Choice struct {
|
|||||||
// CommandApp orchestrates Discord command use cases against ha-gateway.
|
// CommandApp orchestrates Discord command use cases against ha-gateway.
|
||||||
type CommandApp struct {
|
type CommandApp struct {
|
||||||
ha driven.HAGateway
|
ha driven.HAGateway
|
||||||
|
ai driven.AIGateway
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCommandApp constructs the Discord command application service.
|
// NewCommandApp constructs the Discord command application service.
|
||||||
func NewCommandApp(ha driven.HAGateway) *CommandApp {
|
func NewCommandApp(ha driven.HAGateway, ai driven.AIGateway) *CommandApp {
|
||||||
return &CommandApp{ha: ha}
|
return &CommandApp{ha: ha, ai: ai}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleLightList formats discovered lights into a monospace-friendly response.
|
// HandleLightList formats discovered lights into a monospace-friendly response.
|
||||||
@ -121,6 +122,15 @@ func (a *CommandApp) HandleSwitchList(ctx context.Context) (string, error) {
|
|||||||
return strings.Join(lines, "\n"), nil
|
return strings.Join(lines, "\n"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HandleAIQuery forwards a free-form request to ai-gateway.
|
||||||
|
func (a *CommandApp) HandleAIQuery(ctx context.Context, text string) (string, error) {
|
||||||
|
reply, err := a.ai.Query(ctx, text)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("handle ai query: %w", err)
|
||||||
|
}
|
||||||
|
return reply, nil
|
||||||
|
}
|
||||||
|
|
||||||
// AutocompleteLights maps discovered lights into Discord autocomplete choices.
|
// AutocompleteLights maps discovered lights into Discord autocomplete choices.
|
||||||
func (a *CommandApp) AutocompleteLights(ctx context.Context) ([]Choice, error) {
|
func (a *CommandApp) AutocompleteLights(ctx context.Context) ([]Choice, error) {
|
||||||
lights, err := a.ha.ListLights(ctx)
|
lights, err := a.ha.ListLights(ctx)
|
||||||
|
|||||||
@ -17,6 +17,10 @@ type mockHAGateway struct {
|
|||||||
toggleLightFunc func(ctx context.Context, entityID string) error
|
toggleLightFunc func(ctx context.Context, entityID string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type mockAIGateway struct {
|
||||||
|
queryFunc func(ctx context.Context, text string) (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
func (m *mockHAGateway) ListLights(ctx context.Context) ([]driven.Light, error) {
|
func (m *mockHAGateway) ListLights(ctx context.Context) ([]driven.Light, error) {
|
||||||
if m.listLightsFunc == nil {
|
if m.listLightsFunc == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@ -52,6 +56,13 @@ func (m *mockHAGateway) ToggleLight(ctx context.Context, entityID string) error
|
|||||||
return m.toggleLightFunc(ctx, entityID)
|
return m.toggleLightFunc(ctx, entityID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *mockAIGateway) Query(ctx context.Context, text string) (string, error) {
|
||||||
|
if m.queryFunc == nil {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
return m.queryFunc(ctx, text)
|
||||||
|
}
|
||||||
|
|
||||||
func TestCommandAppHandleLightList(t *testing.T) {
|
func TestCommandAppHandleLightList(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@ -95,7 +106,7 @@ func TestCommandAppHandleLightList(t *testing.T) {
|
|||||||
listLightsFunc: func(ctx context.Context) ([]driven.Light, error) {
|
listLightsFunc: func(ctx context.Context) ([]driven.Light, error) {
|
||||||
return tt.lights, nil
|
return tt.lights, nil
|
||||||
},
|
},
|
||||||
})
|
}, &mockAIGateway{})
|
||||||
|
|
||||||
got, err := app.HandleLightList(context.Background())
|
got, err := app.HandleLightList(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -185,7 +196,7 @@ func TestCommandAppHandleLightOn(t *testing.T) {
|
|||||||
gotColorTemp = colorTempKelvin
|
gotColorTemp = colorTempKelvin
|
||||||
return tt.turnOnErr
|
return tt.turnOnErr
|
||||||
},
|
},
|
||||||
})
|
}, &mockAIGateway{})
|
||||||
|
|
||||||
got, err := app.HandleLightOn(context.Background(), tt.entityID, tt.brightnessPct, tt.colorTempKelvin)
|
got, err := app.HandleLightOn(context.Background(), tt.entityID, tt.brightnessPct, tt.colorTempKelvin)
|
||||||
if tt.wantErr != "" {
|
if tt.wantErr != "" {
|
||||||
@ -255,7 +266,7 @@ func TestCommandAppHandleLightOff(t *testing.T) {
|
|||||||
gotTransition = transition
|
gotTransition = transition
|
||||||
return tt.turnOffErr
|
return tt.turnOffErr
|
||||||
},
|
},
|
||||||
})
|
}, &mockAIGateway{})
|
||||||
|
|
||||||
got, err := app.HandleLightOff(context.Background(), tt.entityID, tt.transition)
|
got, err := app.HandleLightOff(context.Background(), tt.entityID, tt.transition)
|
||||||
if tt.wantErr != "" {
|
if tt.wantErr != "" {
|
||||||
@ -316,7 +327,7 @@ func TestCommandAppHandleLightToggle(t *testing.T) {
|
|||||||
toggleLightFunc: func(ctx context.Context, entityID string) error {
|
toggleLightFunc: func(ctx context.Context, entityID string) error {
|
||||||
return tt.toggleErr
|
return tt.toggleErr
|
||||||
},
|
},
|
||||||
})
|
}, &mockAIGateway{})
|
||||||
|
|
||||||
got, err := app.HandleLightToggle(context.Background(), "light.kitchen")
|
got, err := app.HandleLightToggle(context.Background(), "light.kitchen")
|
||||||
if tt.wantErr != "" {
|
if tt.wantErr != "" {
|
||||||
@ -361,7 +372,7 @@ func TestCommandAppHandleSwitchList(t *testing.T) {
|
|||||||
listSwitchesFunc: func(ctx context.Context) ([]driven.Switch, error) {
|
listSwitchesFunc: func(ctx context.Context) ([]driven.Switch, error) {
|
||||||
return tt.switches, nil
|
return tt.switches, nil
|
||||||
},
|
},
|
||||||
})
|
}, &mockAIGateway{})
|
||||||
|
|
||||||
got, err := app.HandleSwitchList(context.Background())
|
got, err := app.HandleSwitchList(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -409,7 +420,7 @@ func TestCommandAppAutocompleteLights(t *testing.T) {
|
|||||||
}
|
}
|
||||||
return tt.lights, nil
|
return tt.lights, nil
|
||||||
},
|
},
|
||||||
})
|
}, &mockAIGateway{})
|
||||||
|
|
||||||
got, err := app.AutocompleteLights(context.Background())
|
got, err := app.AutocompleteLights(context.Background())
|
||||||
if tt.wantErr != "" {
|
if tt.wantErr != "" {
|
||||||
@ -463,7 +474,7 @@ func TestCommandAppAutocompleteSwitches(t *testing.T) {
|
|||||||
}
|
}
|
||||||
return tt.switches, nil
|
return tt.switches, nil
|
||||||
},
|
},
|
||||||
})
|
}, &mockAIGateway{})
|
||||||
|
|
||||||
got, err := app.AutocompleteSwitches(context.Background())
|
got, err := app.AutocompleteSwitches(context.Background())
|
||||||
if tt.wantErr != "" {
|
if tt.wantErr != "" {
|
||||||
@ -481,3 +492,22 @@ func TestCommandAppAutocompleteSwitches(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCommandAppHandleAIQuery(t *testing.T) {
|
||||||
|
app := NewCommandApp(&mockHAGateway{}, &mockAIGateway{
|
||||||
|
queryFunc: func(ctx context.Context, text string) (string, error) {
|
||||||
|
if text != "turn on kitchen" {
|
||||||
|
t.Fatalf("Query() text = %q", text)
|
||||||
|
}
|
||||||
|
return "Turning on Kitchen.", nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
got, err := app.HandleAIQuery(context.Background(), "turn on kitchen")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("HandleAIQuery() error = %v", err)
|
||||||
|
}
|
||||||
|
if got != "Turning on Kitchen." {
|
||||||
|
t.Fatalf("HandleAIQuery() = %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -10,6 +10,7 @@ type Config struct {
|
|||||||
DiscordToken string
|
DiscordToken string
|
||||||
GuildID string
|
GuildID string
|
||||||
HAGatewayAddr string
|
HAGatewayAddr string
|
||||||
|
AIGatewayAddr string
|
||||||
TLSDir string
|
TLSDir string
|
||||||
OTELEndpoint string
|
OTELEndpoint string
|
||||||
LogLevel string
|
LogLevel string
|
||||||
@ -32,6 +33,7 @@ func Load() (*Config, error) {
|
|||||||
DiscordToken: token,
|
DiscordToken: token,
|
||||||
GuildID: os.Getenv("GUILD_ID"),
|
GuildID: os.Getenv("GUILD_ID"),
|
||||||
HAGatewayAddr: addr,
|
HAGatewayAddr: addr,
|
||||||
|
AIGatewayAddr: getenvDefault("AI_GATEWAY_ADDR", "ai-gateway.home-services.svc.cluster.local:50052"),
|
||||||
TLSDir: os.Getenv("TLS_DIR"),
|
TLSDir: os.Getenv("TLS_DIR"),
|
||||||
OTELEndpoint: os.Getenv("OTEL_ENDPOINT"),
|
OTELEndpoint: os.Getenv("OTEL_ENDPOINT"),
|
||||||
LogLevel: getenvDefault("LOG_LEVEL", "info"),
|
LogLevel: getenvDefault("LOG_LEVEL", "info"),
|
||||||
|
|||||||
8
discord-bot/internal/core/ports/driven/ai.go
Normal file
8
discord-bot/internal/core/ports/driven/ai.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package driven
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
// AIGateway exposes the free-form AI query API used by the Discord bot.
|
||||||
|
type AIGateway interface {
|
||||||
|
Query(ctx context.Context, text string) (string, error)
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user