diff --git a/cmd/picoclaw/internal/gateway/helpers.go b/cmd/picoclaw/internal/gateway/helpers.go index 2d84d9e0a..fed3d5ffb 100644 --- a/cmd/picoclaw/internal/gateway/helpers.go +++ b/cmd/picoclaw/internal/gateway/helpers.go @@ -25,7 +25,6 @@ import ( _ "github.com/sipeed/picoclaw/pkg/channels/qq" _ "github.com/sipeed/picoclaw/pkg/channels/slack" _ "github.com/sipeed/picoclaw/pkg/channels/telegram" - tgchannel "github.com/sipeed/picoclaw/pkg/channels/telegram" _ "github.com/sipeed/picoclaw/pkg/channels/wecom" _ "github.com/sipeed/picoclaw/pkg/channels/whatsapp" _ "github.com/sipeed/picoclaw/pkg/channels/whatsapp_native" @@ -144,12 +143,6 @@ func gatewayCmd(debug bool) error { logger.InfoCF("voice", "Transcription enabled (agent-level)", map[string]any{"provider": transcriber.Name()}) } - if telegramCh, ok := channelManager.GetChannel("telegram"); ok { - if tc, ok := telegramCh.(*tgchannel.TelegramChannel); ok { - tc.SetRegistry(agentLoop.GetRegistry()) - } - } - enabledChannels := channelManager.GetEnabledChannels() if len(enabledChannels) > 0 { fmt.Printf("✓ Channels enabled: %s\n", enabledChannels) diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index 76c7847c1..02015eb27 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -1812,12 +1812,18 @@ func (al *AgentLoop) buildCommandsRuntime(agent *AgentInstance, opts *processOpt } if agent != nil { rt.GetModelInfo = func() (string, string) { - return agent.Model, al.cfg.Agents.Defaults.Provider + provider := al.cfg.Agents.Defaults.Provider + if len(agent.Candidates) > 0 && agent.Candidates[0].Provider != "" { + provider = agent.Candidates[0].Provider + } + return agent.Model, provider } rt.SwitchModel = func(value string) (string, error) { - oldModel := agent.Model - agent.Model = value - return oldModel, nil + if al.registry == nil { + return "", fmt.Errorf("agent registry not initialized") + } + oldModel, _, err := al.registry.SwitchDefaultAgentModel(value) + return oldModel, err } rt.ClearHistory = func() error { diff --git a/pkg/agent/loop_test.go b/pkg/agent/loop_test.go index 2e456fa60..9121803d5 100644 --- a/pkg/agent/loop_test.go +++ b/pkg/agent/loop_test.go @@ -531,10 +531,23 @@ func TestProcessMessage_SwitchModelShowModelConsistency(t *testing.T) { Workspace: tmpDir, Provider: "openai", Model: "before-switch", + ModelName: "test-openai-mini", MaxTokens: 4096, MaxToolIterations: 10, }, }, + ModelList: []config.ModelConfig{ + { + ModelName: "test-openai-mini", + Model: "openai/fake-openai-mini", + APIKey: "test-openai-key", + }, + { + ModelName: "test-qwen-plus", + Model: "qwen/fake-qwen-plus", + APIKey: "test-qwen-key", + }, + }, } msgBus := bus.NewMessageBus() @@ -546,13 +559,13 @@ func TestProcessMessage_SwitchModelShowModelConsistency(t *testing.T) { Channel: "telegram", SenderID: "user1", ChatID: "chat1", - Content: "/switch model to after-switch", + Content: "/switch model to test-qwen-plus", Peer: bus.Peer{ Kind: "direct", ID: "user1", }, }) - if !strings.Contains(switchResp, "Switched model from before-switch to after-switch") { + if !strings.Contains(switchResp, "Switched model from test-openai-mini to fake-qwen-plus") { t.Fatalf("unexpected /switch reply: %q", switchResp) } @@ -566,7 +579,7 @@ func TestProcessMessage_SwitchModelShowModelConsistency(t *testing.T) { ID: "user1", }, }) - if !strings.Contains(showResp, "Current Model: after-switch (Provider: openai)") { + if !strings.Contains(showResp, "Current Model: fake-qwen-plus (Provider: qwen)") { t.Fatalf("unexpected /show model reply after switch: %q", showResp) } diff --git a/pkg/channels/telegram/telegram.go b/pkg/channels/telegram/telegram.go index 2462df1a9..b04beeb6e 100644 --- a/pkg/channels/telegram/telegram.go +++ b/pkg/channels/telegram/telegram.go @@ -40,13 +40,12 @@ var ( type TelegramChannel struct { *channels.BaseChannel - bot *telego.Bot - bh *th.BotHandler - commands TelegramCommander - config *config.Config - chatIDs map[string]int64 - ctx context.Context - cancel context.CancelFunc + bot *telego.Bot + bh *th.BotHandler + config *config.Config + chatIDs map[string]int64 + ctx context.Context + cancel context.CancelFunc registerFunc func(context.Context, []commands.Definition) error commandRegCancel context.CancelFunc @@ -96,17 +95,12 @@ func NewTelegramChannel(cfg *config.Config, bus *bus.MessageBus) (*TelegramChann return &TelegramChannel{ BaseChannel: base, - commands: NewTelegramCommands(bot, cfg, nil), bot: bot, config: cfg, chatIDs: make(map[string]int64), }, nil } -func (c *TelegramChannel) SetRegistry(switcher AgentModelSwitcher) { - c.commands = NewTelegramCommands(c.bot, c.config, switcher) -} - func (c *TelegramChannel) Start(ctx context.Context) error { logger.InfoC("telegram", "Starting Telegram bot (polling mode)...") @@ -127,25 +121,6 @@ func (c *TelegramChannel) Start(ctx context.Context) error { } c.bh = bh - bh.HandleMessage(func(ctx *th.Context, message telego.Message) error { - return c.commands.Start(ctx, message) - }, th.CommandEqual("start")) - bh.HandleMessage(func(ctx *th.Context, message telego.Message) error { - return c.commands.Help(ctx, message) - }, th.CommandEqual("help")) - - bh.HandleMessage(func(ctx *th.Context, message telego.Message) error { - return c.commands.Show(ctx, message) - }, th.CommandEqual("show")) - - bh.HandleMessage(func(ctx *th.Context, message telego.Message) error { - return c.commands.List(ctx, message) - }, th.CommandEqual("list")) - - bh.HandleMessage(func(ctx *th.Context, message telego.Message) error { - return c.commands.Model(ctx, message) - }, th.CommandEqual("model")) - bh.HandleMessage(func(ctx *th.Context, message telego.Message) error { return c.handleMessage(ctx, &message) }, th.AnyMessage()) @@ -155,11 +130,7 @@ func (c *TelegramChannel) Start(ctx context.Context) error { "username": c.bot.Username(), }) - commandDefs := append(commands.BuiltinDefinitions(), commands.Definition{ - Name: "model", - Description: "Show or switch the active model", - }) - c.startCommandRegistration(c.ctx, commandDefs) + c.startCommandRegistration(c.ctx, commands.BuiltinDefinitions()) go func() { if err = bh.Start(); err != nil { diff --git a/pkg/channels/telegram/telegram_commands.go b/pkg/channels/telegram/telegram_commands.go deleted file mode 100644 index e603b0117..000000000 --- a/pkg/channels/telegram/telegram_commands.go +++ /dev/null @@ -1,234 +0,0 @@ -package telegram - -import ( - "context" - "fmt" - "strings" - - "github.com/mymmrac/telego" - - "github.com/sipeed/picoclaw/pkg/config" - "github.com/sipeed/picoclaw/pkg/providers" -) - -// AgentModelSwitcher is the minimal interface needed to get/set the active model. -type AgentModelSwitcher interface { - GetDefaultAgentModel() string - SwitchDefaultAgentModel(modelName string) (string, string, error) -} - -type TelegramCommander interface { - Help(ctx context.Context, message telego.Message) error - Start(ctx context.Context, message telego.Message) error - Show(ctx context.Context, message telego.Message) error - List(ctx context.Context, message telego.Message) error - Model(ctx context.Context, message telego.Message) error -} - -type cmd struct { - bot *telego.Bot - config *config.Config - switcher AgentModelSwitcher -} - -func NewTelegramCommands(bot *telego.Bot, cfg *config.Config, switcher AgentModelSwitcher) TelegramCommander { - return &cmd{ - bot: bot, - config: cfg, - switcher: switcher, - } -} - -func commandArgs(text string) string { - parts := strings.SplitN(text, " ", 2) - if len(parts) < 2 { - return "" - } - return strings.TrimSpace(parts[1]) -} - -func (c *cmd) Help(ctx context.Context, message telego.Message) error { - msg := `/start - Start the bot -/help - Show this help message -/show [model|channel] - Show current configuration -/list [models|channels] - List available options -/model - Show or switch the active model - /model - show current model - /model list - list available models - /model - switch to named model - ` - _, err := c.bot.SendMessage(ctx, &telego.SendMessageParams{ - ChatID: telego.ChatID{ID: message.Chat.ID}, - Text: msg, - ReplyParameters: &telego.ReplyParameters{ - MessageID: message.MessageID, - }, - }) - return err -} - -func (c *cmd) Start(ctx context.Context, message telego.Message) error { - _, err := c.bot.SendMessage(ctx, &telego.SendMessageParams{ - ChatID: telego.ChatID{ID: message.Chat.ID}, - Text: "Hello! I am PicoClaw 🦞", - ReplyParameters: &telego.ReplyParameters{ - MessageID: message.MessageID, - }, - }) - return err -} - -func (c *cmd) Show(ctx context.Context, message telego.Message) error { - args := commandArgs(message.Text) - if args == "" { - _, err := c.bot.SendMessage(ctx, &telego.SendMessageParams{ - ChatID: telego.ChatID{ID: message.Chat.ID}, - Text: "Usage: /show [model|channel]", - ReplyParameters: &telego.ReplyParameters{ - MessageID: message.MessageID, - }, - }) - return err - } - - var response string - switch args { - case "model": - if c.switcher == nil { - response = fmt.Sprintf("Current model: %s", c.config.Agents.Defaults.GetModelName()) - } else { - response = fmt.Sprintf("Current model: %s", c.switcher.GetDefaultAgentModel()) - } - case "channel": - response = "Current Channel: telegram" - default: - response = fmt.Sprintf("Unknown parameter: %s. Try 'model' or 'channel'.", args) - } - - _, err := c.bot.SendMessage(ctx, &telego.SendMessageParams{ - ChatID: telego.ChatID{ID: message.Chat.ID}, - Text: response, - ReplyParameters: &telego.ReplyParameters{ - MessageID: message.MessageID, - }, - }) - return err -} - -func (c *cmd) List(ctx context.Context, message telego.Message) error { - args := commandArgs(message.Text) - if args == "" { - _, err := c.bot.SendMessage(ctx, &telego.SendMessageParams{ - ChatID: telego.ChatID{ID: message.Chat.ID}, - Text: "Usage: /list [models|channels]", - ReplyParameters: &telego.ReplyParameters{ - MessageID: message.MessageID, - }, - }) - return err - } - - var response string - switch args { - case "models": - provider := c.config.Agents.Defaults.Provider - if provider == "" { - provider = "configured default" - } - response = fmt.Sprintf("Configured Model: %s\nProvider: %s\n\nTo change models, update config.json", - c.config.Agents.Defaults.GetModelName(), provider) - - case "channels": - var enabled []string - if c.config.Channels.Telegram.Enabled { - enabled = append(enabled, "telegram") - } - if c.config.Channels.WhatsApp.Enabled { - enabled = append(enabled, "whatsapp") - } - if c.config.Channels.Feishu.Enabled { - enabled = append(enabled, "feishu") - } - if c.config.Channels.Discord.Enabled { - enabled = append(enabled, "discord") - } - if c.config.Channels.Slack.Enabled { - enabled = append(enabled, "slack") - } - response = fmt.Sprintf("Enabled Channels:\n- %s", strings.Join(enabled, "\n- ")) - - default: - response = fmt.Sprintf("Unknown parameter: %s. Try 'models' or 'channels'.", args) - } - - _, err := c.bot.SendMessage(ctx, &telego.SendMessageParams{ - ChatID: telego.ChatID{ID: message.Chat.ID}, - Text: response, - ReplyParameters: &telego.ReplyParameters{ - MessageID: message.MessageID, - }, - }) - return err -} - -func (c *cmd) Model(ctx context.Context, message telego.Message) error { - args := commandArgs(message.Text) - - var response string - switch { - case args == "": - // Show current model - if c.switcher == nil { - response = "No agent configured." - } else { - response = fmt.Sprintf("Current model: %s", c.switcher.GetDefaultAgentModel()) - } - - case args == "list": - // List models from config - if len(c.config.ModelList) == 0 { - response = "No models configured in model_list." - } else { - currentModel := "" - if c.switcher != nil { - currentModel = c.switcher.GetDefaultAgentModel() - } - lines := make([]string, 0, len(c.config.ModelList)) - for _, m := range c.config.ModelList { - _, modelID := providers.ExtractProtocol(m.Model) - line := "• " + m.ModelName - if modelID == currentModel { - line += " (active)" - } - line += " -> " + m.Model - lines = append(lines, line) - } - response = "Available models:\n" + strings.Join(lines, "\n") - } - - default: - // Switch to the named model - modelName := args - if c.switcher == nil { - response = "No agent configured." - } else { - oldModel, newModel, err := c.switcher.SwitchDefaultAgentModel(modelName) - if err != nil { - response = fmt.Sprintf("Failed to switch model: %v", err) - } else if oldModel == newModel { - response = fmt.Sprintf("Model already active: %s", newModel) - } else { - response = fmt.Sprintf("Switched model: %s -> %s", oldModel, newModel) - } - } - } - - _, err := c.bot.SendMessage(ctx, &telego.SendMessageParams{ - ChatID: telego.ChatID{ID: message.Chat.ID}, - Text: response, - ReplyParameters: &telego.ReplyParameters{ - MessageID: message.MessageID, - }, - }) - return err -} diff --git a/pkg/commands/cmd_switch.go b/pkg/commands/cmd_switch.go index fb8fc109e..00fbd0d26 100644 --- a/pkg/commands/cmd_switch.go +++ b/pkg/commands/cmd_switch.go @@ -27,7 +27,14 @@ func switchCommand() Definition { if err != nil { return req.Reply(err.Error()) } - return req.Reply(fmt.Sprintf("Switched model from %s to %s", oldModel, value)) + newModel := value + if rt.GetModelInfo != nil { + currentModel, _ := rt.GetModelInfo() + if currentModel != "" { + newModel = currentModel + } + } + return req.Reply(fmt.Sprintf("Switched model from %s to %s", oldModel, newModel)) }, }, { diff --git a/pkg/commands/cmd_switch_test.go b/pkg/commands/cmd_switch_test.go index 59ed305bb..a6ca4e3bd 100644 --- a/pkg/commands/cmd_switch_test.go +++ b/pkg/commands/cmd_switch_test.go @@ -11,6 +11,9 @@ func TestSwitchModel_Success(t *testing.T) { SwitchModel: func(value string) (string, error) { return "old-model", nil }, + GetModelInfo: func() (string, string) { + return "openai/test-model", "openai" + }, } ex := NewExecutor(NewRegistry(BuiltinDefinitions()), rt) @@ -25,7 +28,7 @@ func TestSwitchModel_Success(t *testing.T) { if res.Outcome != OutcomeHandled { t.Fatalf("outcome=%v, want=%v", res.Outcome, OutcomeHandled) } - want := "Switched model from old-model to gpt-4" + want := "Switched model from old-model to openai/test-model" if reply != want { t.Fatalf("reply=%q, want=%q", reply, want) } @@ -239,6 +242,9 @@ func TestSwitch_BangPrefix(t *testing.T) { SwitchModel: func(value string) (string, error) { return "old", nil }, + GetModelInfo: func() (string, string) { + return "openai/test-model", "openai" + }, } ex := NewExecutor(NewRegistry(BuiltinDefinitions()), rt) @@ -253,7 +259,7 @@ func TestSwitch_BangPrefix(t *testing.T) { if res.Outcome != OutcomeHandled { t.Fatalf("! prefix: outcome=%v, want=%v", res.Outcome, OutcomeHandled) } - if reply != "Switched model from old to gpt-4" { + if reply != "Switched model from old to openai/test-model" { t.Fatalf("! prefix: reply=%q, want success message", reply) } }