mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-12 18:16:09 +00:00
The dashboard /tts page writes to system_configs[tts.<provider>.voice], but the LLM-invoked tts tool was only checking args > agent OtherConfig > builtin_tool_tenant_configs[tts].default_voice_id. Two different storage locations → the user's chosen voice was ignored, Edge defaulted to en-US-AriaNeural even when "HoaiMy" (vi-VN-HoaiMyNeural) was set correctly in the dashboard. Add system_configs as a 4th-level fallback so the dashboard becomes the single source of truth. - TtsTool gains SetSystemConfigStore(s) setter - resolveVoiceAndModel takes providerName + looks up tts.<provider>.voice/model when no higher-precedence source set - effectiveProvider resolved BEFORE voice/model so the right key is hit - Wired at gateway boot in cmd/gateway.go (after pgStores ready) - 4 unit tests covering: fallback, arg precedence, no-store, empty provider Verified against production trace 019e6036-44bb-703b-85fa-dee34f7ab2c0 where the tts tool was called with provider=edge, no voice arg, and defaulted to English instead of the configured Vietnamese voice.
112 lines
3.3 KiB
Go
112 lines
3.3 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/store"
|
|
)
|
|
|
|
// ctxWithAgentAudio returns a context carrying an AgentAudioSnapshot whose
|
|
// OtherConfig has the given tts_voice_id / tts_model_id keys (either may be
|
|
// empty — empty means "not set by agent config").
|
|
func ctxWithAgentAudio(t *testing.T, voiceID, modelID string) context.Context {
|
|
t.Helper()
|
|
cfg := map[string]string{}
|
|
if voiceID != "" {
|
|
cfg["tts_voice_id"] = voiceID
|
|
}
|
|
if modelID != "" {
|
|
cfg["tts_model_id"] = modelID
|
|
}
|
|
raw, err := json.Marshal(cfg)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return store.WithAgentAudio(context.Background(), store.AgentAudioSnapshot{
|
|
AgentID: uuid.New(),
|
|
OtherConfig: raw,
|
|
})
|
|
}
|
|
|
|
func TestResolveVoiceAndModel_ArgsWinOverAgent(t *testing.T) {
|
|
tool := NewTtsTool(makeTTSManager("elevenlabs"))
|
|
ctx := ctxWithAgentAudio(t, "AGENT_V", "AGENT_M")
|
|
v, m, _ := tool.resolveVoiceAndModel(ctx, "edge", "ARG_V", "ARG_M")
|
|
if v != "ARG_V" {
|
|
t.Errorf("voice: got %q, want ARG_V (args must win)", v)
|
|
}
|
|
if m != "ARG_M" {
|
|
t.Errorf("model: got %q, want ARG_M (args must win)", m)
|
|
}
|
|
}
|
|
|
|
func TestResolveVoiceAndModel_AgentWinsOverTenantWhenArgsEmpty(t *testing.T) {
|
|
tool := NewTtsTool(makeTTSManager("elevenlabs"))
|
|
ctx := ctxWithAgentAudio(t, "AGENT_V", "AGENT_M")
|
|
ctx = WithBuiltinToolSettings(ctx, BuiltinToolSettings{
|
|
"tts": rawJSON(t, map[string]string{"default_voice_id": "TENANT_V", "default_model": "TENANT_M"}),
|
|
})
|
|
v, m, _ := tool.resolveVoiceAndModel(ctx, "edge", "", "")
|
|
if v != "AGENT_V" {
|
|
t.Errorf("voice: got %q, want AGENT_V (agent > tenant)", v)
|
|
}
|
|
if m != "AGENT_M" {
|
|
t.Errorf("model: got %q, want AGENT_M (agent > tenant)", m)
|
|
}
|
|
}
|
|
|
|
func TestResolveVoiceAndModel_TenantFallbackWhenAgentSilent(t *testing.T) {
|
|
tool := NewTtsTool(makeTTSManager("elevenlabs"))
|
|
// No agent audio ctx, only tenant settings.
|
|
ctx := WithBuiltinToolSettings(context.Background(), BuiltinToolSettings{
|
|
"tts": rawJSON(t, map[string]string{"default_voice_id": "TENANT_V", "default_model": "TENANT_M"}),
|
|
})
|
|
v, m, _ := tool.resolveVoiceAndModel(ctx, "edge", "", "")
|
|
if v != "TENANT_V" {
|
|
t.Errorf("voice: got %q, want TENANT_V", v)
|
|
}
|
|
if m != "TENANT_M" {
|
|
t.Errorf("model: got %q, want TENANT_M", m)
|
|
}
|
|
}
|
|
|
|
func TestResolveVoiceAndModel_EmptyAllMeansDefault(t *testing.T) {
|
|
tool := NewTtsTool(makeTTSManager("elevenlabs"))
|
|
v, m, _ := tool.resolveVoiceAndModel(context.Background(), "edge", "", "")
|
|
if v != "" {
|
|
t.Errorf("voice: got %q, want empty (no sources → provider default)", v)
|
|
}
|
|
if m != "" {
|
|
t.Errorf("model: got %q, want empty (no sources → provider default)", m)
|
|
}
|
|
}
|
|
|
|
func TestResolveVoiceAndModel_PartialAgentConfig(t *testing.T) {
|
|
tool := NewTtsTool(makeTTSManager("elevenlabs"))
|
|
// Agent only provides voice; model must fall to tenant.
|
|
ctx := ctxWithAgentAudio(t, "AGENT_V", "")
|
|
ctx = WithBuiltinToolSettings(ctx, BuiltinToolSettings{
|
|
"tts": rawJSON(t, map[string]string{"default_model": "TENANT_M"}),
|
|
})
|
|
v, m, _ := tool.resolveVoiceAndModel(ctx, "edge", "", "")
|
|
if v != "AGENT_V" {
|
|
t.Errorf("voice: got %q, want AGENT_V", v)
|
|
}
|
|
if m != "TENANT_M" {
|
|
t.Errorf("model: got %q, want TENANT_M (agent silent → tenant)", m)
|
|
}
|
|
}
|
|
|
|
func rawJSON(t *testing.T, v any) json.RawMessage {
|
|
t.Helper()
|
|
b, err := json.Marshal(v)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return b
|
|
}
|