mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-07-12 13:04:57 +00:00
feat(tools): tenant-aware TTS primary provider resolution
TTS tool now reads per-tenant primary provider override from BuiltinToolSettingsFromCtx before falling back to the manager's default. Same per-call resolution pattern as web_search/web_fetch. Simpler path A approach (no pool) — tenant can select which registered provider their agents use. Per-tenant API keys deferred to a future pool implementation if needed. - Add resolvePrimary(ctx, mgr) with ttsOverride struct - Refactor Execute to use resolved primary with fallback chain - 5 new unit tests for tenant provider override scenarios
This commit is contained in:
+40
-2
@@ -2,7 +2,9 @@ package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
@@ -62,6 +64,32 @@ func (t *TtsTool) Parameters() map[string]any {
|
||||
}
|
||||
}
|
||||
|
||||
// ttsOverride is the tenant settings shape for tts
|
||||
// (stored in builtin_tool_tenant_configs.settings).
|
||||
type ttsOverride struct {
|
||||
Primary string `json:"primary,omitempty"` // override primary provider name
|
||||
}
|
||||
|
||||
// resolvePrimary returns the effective primary provider name for the request.
|
||||
// Checks tenant override via BuiltinToolSettingsFromCtx first.
|
||||
func (t *TtsTool) resolvePrimary(ctx context.Context, mgr *tts.Manager) string {
|
||||
if settings := BuiltinToolSettingsFromCtx(ctx); settings != nil {
|
||||
if raw, ok := settings["tts"]; ok && len(raw) > 0 {
|
||||
var override ttsOverride
|
||||
if err := json.Unmarshal(raw, &override); err != nil {
|
||||
slog.Warn("tts: failed to parse tenant override, using defaults", "error", err)
|
||||
} else if override.Primary != "" {
|
||||
// Verify the provider exists in the manager
|
||||
if _, exists := mgr.GetProvider(override.Primary); exists {
|
||||
return override.Primary
|
||||
}
|
||||
slog.Warn("tts: tenant override references unknown provider", "primary", override.Primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
return mgr.PrimaryProvider()
|
||||
}
|
||||
|
||||
// SetContext is a no-op; channel is now read from ctx (thread-safe).
|
||||
func (t *TtsTool) SetContext(channel, _ string) {}
|
||||
|
||||
@@ -90,14 +118,24 @@ func (t *TtsTool) Execute(ctx context.Context, args map[string]any) *Result {
|
||||
var err error
|
||||
|
||||
if providerName != "" {
|
||||
// Use specific provider
|
||||
// Use specific provider (explicit call param takes precedence)
|
||||
p, ok := mgr.GetProvider(providerName)
|
||||
if !ok {
|
||||
return &Result{ForLLM: fmt.Sprintf("error: tts provider not found: %s", providerName), IsError: true}
|
||||
}
|
||||
result, err = p.Synthesize(ctx, text, opts)
|
||||
} else {
|
||||
result, err = mgr.SynthesizeWithFallback(ctx, text, opts)
|
||||
// Resolve primary from tenant settings or default
|
||||
primary := t.resolvePrimary(ctx, mgr)
|
||||
if p, ok := mgr.GetProvider(primary); ok {
|
||||
result, err = p.Synthesize(ctx, text, opts)
|
||||
if err != nil {
|
||||
slog.Warn("tts primary provider failed, trying fallback", "provider", primary, "error", err)
|
||||
result, err = mgr.SynthesizeWithFallback(ctx, text, opts)
|
||||
}
|
||||
} else {
|
||||
result, err = mgr.SynthesizeWithFallback(ctx, text, opts)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/tts"
|
||||
)
|
||||
|
||||
func makeTTSManager(providers ...string) *tts.Manager {
|
||||
mgr := tts.NewManager(tts.ManagerConfig{Primary: providers[0]})
|
||||
for _, name := range providers {
|
||||
mgr.RegisterProvider(&fakeTTSProvider{name: name})
|
||||
}
|
||||
return mgr
|
||||
}
|
||||
|
||||
// fakeTTSProvider is a no-op provider for testing provider resolution.
|
||||
type fakeTTSProvider struct{ name string }
|
||||
|
||||
func (f *fakeTTSProvider) Name() string { return f.name }
|
||||
func (f *fakeTTSProvider) Synthesize(_ context.Context, _ string, _ tts.Options) (*tts.SynthResult, error) {
|
||||
return &tts.SynthResult{Audio: []byte("fake"), Extension: "mp3"}, nil
|
||||
}
|
||||
|
||||
func ctxWithTTSSettings(t *testing.T, override ttsOverride) context.Context {
|
||||
t.Helper()
|
||||
raw, err := json.Marshal(override)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
settings := BuiltinToolSettings{"tts": raw}
|
||||
return WithBuiltinToolSettings(context.Background(), settings)
|
||||
}
|
||||
|
||||
func TestResolvePrimary_NoOverride(t *testing.T) {
|
||||
tool := NewTtsTool(makeTTSManager("openai", "edge"))
|
||||
got := tool.resolvePrimary(context.Background(), tool.manager)
|
||||
if got != "openai" {
|
||||
t.Errorf("got %q, want openai", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolvePrimary_TenantOverride(t *testing.T) {
|
||||
tool := NewTtsTool(makeTTSManager("openai", "elevenlabs", "edge"))
|
||||
ctx := ctxWithTTSSettings(t, ttsOverride{Primary: "elevenlabs"})
|
||||
got := tool.resolvePrimary(ctx, tool.manager)
|
||||
if got != "elevenlabs" {
|
||||
t.Errorf("got %q, want elevenlabs", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolvePrimary_UnknownProvider_FallsBack(t *testing.T) {
|
||||
tool := NewTtsTool(makeTTSManager("openai", "edge"))
|
||||
ctx := ctxWithTTSSettings(t, ttsOverride{Primary: "nonexistent"})
|
||||
got := tool.resolvePrimary(ctx, tool.manager)
|
||||
if got != "openai" {
|
||||
t.Errorf("got %q, want openai (fallback)", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolvePrimary_MalformedJSON_FallsBack(t *testing.T) {
|
||||
tool := NewTtsTool(makeTTSManager("edge"))
|
||||
settings := BuiltinToolSettings{"tts": []byte(`{bad json`)}
|
||||
ctx := WithBuiltinToolSettings(context.Background(), settings)
|
||||
got := tool.resolvePrimary(ctx, tool.manager)
|
||||
if got != "edge" {
|
||||
t.Errorf("got %q, want edge (fallback)", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolvePrimary_EmptyOverride_FallsBack(t *testing.T) {
|
||||
tool := NewTtsTool(makeTTSManager("openai"))
|
||||
ctx := ctxWithTTSSettings(t, ttsOverride{})
|
||||
got := tool.resolvePrimary(ctx, tool.manager)
|
||||
if got != "openai" {
|
||||
t.Errorf("got %q, want openai (default)", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user