mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-14 18:18:37 +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:
1 parent
def1712f3b
commit
43ee918b9e
2 files changed
+120
-2
No files matched your search
+40
-2
@@ -2,7 +2,9 @@ package tools
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
"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).
|
// SetContext is a no-op; channel is now read from ctx (thread-safe).
|
||||||
func (t *TtsTool) SetContext(channel, _ string) {}
|
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
|
var err error
|
||||||
|
|
||||||
if providerName != "" {
|
if providerName != "" {
|
||||||
// Use specific provider
|
// Use specific provider (explicit call param takes precedence)
|
||||||
p, ok := mgr.GetProvider(providerName)
|
p, ok := mgr.GetProvider(providerName)
|
||||||
if !ok {
|
if !ok {
|
||||||
return &Result{ForLLM: fmt.Sprintf("error: tts provider not found: %s", providerName), IsError: true}
|
return &Result{ForLLM: fmt.Sprintf("error: tts provider not found: %s", providerName), IsError: true}
|
||||||
}
|
}
|
||||||
result, err = p.Synthesize(ctx, text, opts)
|
result, err = p.Synthesize(ctx, text, opts)
|
||||||
} else {
|
} 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 {
|
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