From 652bb6bd4c096d5c6dae5546ecac82217640860d Mon Sep 17 00:00:00 2001 From: Huaaudio Date: Sat, 21 Mar 2026 07:18:14 +0100 Subject: [PATCH] update with corrent transcriber and leave_tool --- pkg/agent/loop.go | 3 ++ pkg/asr/groq_transcriber.go | 18 ++++++++++++ pkg/bus/types.go | 1 + pkg/channels/discord/voice.go | 2 ++ pkg/tools/voice_leave.go | 52 +++++++++++++++++++++++++++++++++++ pkg/voice/agent.go | 1 + 6 files changed, 77 insertions(+) create mode 100644 pkg/tools/voice_leave.go diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index a3a0b328d..3d1ccfa99 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -246,6 +246,9 @@ func registerSharedTools( agent.Tools.Register(messageTool) } + // Always register Voice Leave Tool (it inherently checks if channel == "discord") + agent.Tools.Register(tools.NewVoiceLeaveTool(msgBus)) + // Send file tool (outbound media via MediaStore — store injected later by SetMediaStore) if cfg.Tools.IsToolEnabled("send_file") { sendFileTool := tools.NewSendFileTool( diff --git a/pkg/asr/groq_transcriber.go b/pkg/asr/groq_transcriber.go index a17850cb0..b0a98d858 100644 --- a/pkg/asr/groq_transcriber.go +++ b/pkg/asr/groq_transcriber.go @@ -10,8 +10,10 @@ import ( "net/http" "os" "path/filepath" + "strings" "time" + "github.com/sipeed/picoclaw/pkg/config" "github.com/sipeed/picoclaw/pkg/logger" "github.com/sipeed/picoclaw/pkg/utils" ) @@ -166,3 +168,19 @@ func (t *GroqTranscriber) doRequest(ctx context.Context, requestBody *bytes.Buff func (t *GroqTranscriber) Name() string { return "groq" } + +// DetectTranscriber inspects cfg and returns the appropriate Transcriber, or +// nil if no supported transcription provider is configured. +func DetectTranscriber(cfg *config.Config) Transcriber { + // Direct Groq provider config takes priority. + if key := cfg.Providers.Groq.APIKey; key != "" { + return NewGroqTranscriber(key) + } + // Fall back to any model-list entry that uses the groq/ protocol or is explicitly named groq. + for _, mc := range cfg.ModelList { + if (strings.HasPrefix(mc.Model, "groq/") || mc.ModelName == "groq" || mc.Model == "whisper-large-v3-turbo") && mc.APIKey != "" { + return NewGroqTranscriber(mc.APIKey) + } + } + return nil +} diff --git a/pkg/bus/types.go b/pkg/bus/types.go index 15aebc345..27cf61b5f 100644 --- a/pkg/bus/types.go +++ b/pkg/bus/types.go @@ -70,6 +70,7 @@ type AudioChunk struct { // VoiceControl represents state or commands for voice sessions. type VoiceControl struct { SessionID string `json:"session_id"` + ChatID string `json:"chat_id"` Type string `json:"type"` // "state", "command" Action string `json:"action"` // "idle", "listening", "start", "stop", "leave" } diff --git a/pkg/channels/discord/voice.go b/pkg/channels/discord/voice.go index 233cfcccb..6f9e10dfa 100644 --- a/pkg/channels/discord/voice.go +++ b/pkg/channels/discord/voice.go @@ -23,6 +23,7 @@ func (c *DiscordChannel) handleVoiceCommand(s *discordgo.Session, m *discordgo.M logger.InfoCF("discord", "Joining voice channel", map[string]any{"channel": vs.ChannelID}) vc, err := s.ChannelVoiceJoin(c.ctx, m.GuildID, vs.ChannelID, false, false) + vc, err := s.ChannelVoiceJoin(c.ctx, m.GuildID, vs.ChannelID, false, false) if err != nil { s.ChannelMessageSend(m.ChannelID, fmt.Sprintf("Failed to join voice channel: %v", err)) return true @@ -34,6 +35,7 @@ func (c *DiscordChannel) handleVoiceCommand(s *discordgo.Session, m *discordgo.M } else if m.Content == "!vc leave" { vc, exists := s.VoiceConnections[m.GuildID] if exists && vc != nil { + vc.Disconnect(c.ctx) vc.Disconnect(c.ctx) s.ChannelMessageSend(m.ChannelID, "Left Voice Channel.") } else { diff --git a/pkg/tools/voice_leave.go b/pkg/tools/voice_leave.go new file mode 100644 index 000000000..882e7cb9d --- /dev/null +++ b/pkg/tools/voice_leave.go @@ -0,0 +1,52 @@ +package tools + +import ( + "context" + + "github.com/sipeed/picoclaw/pkg/bus" + "github.com/sipeed/picoclaw/pkg/logger" +) + +type VoiceLeaveTool struct { + bus *bus.MessageBus +} + +func NewVoiceLeaveTool(mb *bus.MessageBus) *VoiceLeaveTool { + return &VoiceLeaveTool{bus: mb} +} + +func (t *VoiceLeaveTool) Name() string { + return "voice_leave" +} + +func (t *VoiceLeaveTool) Description() string { + return "Disconnects the bot from the current voice channel. Use this tool when the user says goodbye or explicitly asks you to leave the voice chat." +} + +func (t *VoiceLeaveTool) Parameters() map[string]any { + return map[string]any{ + "type": "object", + "properties": map[string]any{}, + } +} + +func (t *VoiceLeaveTool) Execute(ctx context.Context, args map[string]any) *ToolResult { + channel := ToolChannel(ctx) + chatID := ToolChatID(ctx) + + if channel != "discord" { + return &ToolResult{ForLLM: "Can only leave voice channels on Discord", IsError: true} + } + + t.bus.PublishVoiceControl(ctx, bus.VoiceControl{ + ChatID: chatID, + Type: "command", + Action: "leave", + }) + + logger.InfoCF("agent", "Voice command triggered via tool: leave", map[string]any{"chat_id": chatID}) + + return &ToolResult{ + ForLLM: "Successfully sent disconnect command to voice adapter.", + } +} diff --git a/pkg/voice/agent.go b/pkg/voice/agent.go index 0885e6b2e..8cd01c338 100644 --- a/pkg/voice/agent.go +++ b/pkg/voice/agent.go @@ -243,6 +243,7 @@ func (a *Agent) processUtterance(ctx context.Context, acc *speechAccumulator) { SenderID: acc.speakerID, ChatID: acc.chatID, Content: res.Text + oralPrompt, + Content: res.Text + oralPrompt, Peer: bus.Peer{Kind: "channel", ID: acc.chatID}, Metadata: map[string]string{ "is_voice": "true",