Forward LM reasoning to configured channels
Add reasoning_channel_id to channel configs and the example file. Parse and store Reasoning and ReasoningDetails in LLM responses. Emit reasoning from AgentLoop to the configured channel asynchronously, using chunkString to split Telegram messages at a 4096-rune limit to preserve UTF-8. Pass "think": true to provider Chat calls to enable reasoning generation. Include unit tests for chunking, channel lookup, and outbound reasoning publishing.
This commit is contained in:
parent
cb0c8703fb
commit
52c47ce6a8
6 changed files with 309 additions and 6 deletions
|
|
@ -50,6 +50,7 @@
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"token": "YOUR_TELEGRAM_BOT_TOKEN",
|
"token": "YOUR_TELEGRAM_BOT_TOKEN",
|
||||||
"proxy": "",
|
"proxy": "",
|
||||||
|
"reasoning_channel_id": "-1001234567890",
|
||||||
"allow_from": [
|
"allow_from": [
|
||||||
"YOUR_USER_ID"
|
"YOUR_USER_ID"
|
||||||
]
|
]
|
||||||
|
|
@ -57,6 +58,7 @@
|
||||||
"discord": {
|
"discord": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"token": "YOUR_DISCORD_BOT_TOKEN",
|
"token": "YOUR_DISCORD_BOT_TOKEN",
|
||||||
|
"reasoning_channel_id": "123456789012345678",
|
||||||
"allow_from": [],
|
"allow_from": [],
|
||||||
"mention_only": false
|
"mention_only": false
|
||||||
},
|
},
|
||||||
|
|
@ -64,17 +66,20 @@
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"app_id": "YOUR_QQ_APP_ID",
|
"app_id": "YOUR_QQ_APP_ID",
|
||||||
"app_secret": "YOUR_QQ_APP_SECRET",
|
"app_secret": "YOUR_QQ_APP_SECRET",
|
||||||
|
"reasoning_channel_id": "group-123456",
|
||||||
"allow_from": []
|
"allow_from": []
|
||||||
},
|
},
|
||||||
"maixcam": {
|
"maixcam": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"host": "0.0.0.0",
|
"host": "0.0.0.0",
|
||||||
"port": 18790,
|
"port": 18790,
|
||||||
|
"reasoning_channel_id": "maixcam-room-1",
|
||||||
"allow_from": []
|
"allow_from": []
|
||||||
},
|
},
|
||||||
"whatsapp": {
|
"whatsapp": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"bridge_url": "ws://localhost:3001",
|
"bridge_url": "ws://localhost:3001",
|
||||||
|
"reasoning_channel_id": "12025550123@c.us",
|
||||||
"allow_from": []
|
"allow_from": []
|
||||||
},
|
},
|
||||||
"feishu": {
|
"feishu": {
|
||||||
|
|
@ -83,18 +88,21 @@
|
||||||
"app_secret": "",
|
"app_secret": "",
|
||||||
"encrypt_key": "",
|
"encrypt_key": "",
|
||||||
"verification_token": "",
|
"verification_token": "",
|
||||||
|
"reasoning_channel_id": "oc_reasoning_chat_id",
|
||||||
"allow_from": []
|
"allow_from": []
|
||||||
},
|
},
|
||||||
"dingtalk": {
|
"dingtalk": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"client_id": "YOUR_CLIENT_ID",
|
"client_id": "YOUR_CLIENT_ID",
|
||||||
"client_secret": "YOUR_CLIENT_SECRET",
|
"client_secret": "YOUR_CLIENT_SECRET",
|
||||||
|
"reasoning_channel_id": "cid_reasoning_group",
|
||||||
"allow_from": []
|
"allow_from": []
|
||||||
},
|
},
|
||||||
"slack": {
|
"slack": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"bot_token": "xoxb-YOUR-BOT-TOKEN",
|
"bot_token": "xoxb-YOUR-BOT-TOKEN",
|
||||||
"app_token": "xapp-YOUR-APP-TOKEN",
|
"app_token": "xapp-YOUR-APP-TOKEN",
|
||||||
|
"reasoning_channel_id": "C0123456789",
|
||||||
"allow_from": []
|
"allow_from": []
|
||||||
},
|
},
|
||||||
"line": {
|
"line": {
|
||||||
|
|
@ -104,6 +112,7 @@
|
||||||
"webhook_host": "0.0.0.0",
|
"webhook_host": "0.0.0.0",
|
||||||
"webhook_port": 18791,
|
"webhook_port": 18791,
|
||||||
"webhook_path": "/webhook/line",
|
"webhook_path": "/webhook/line",
|
||||||
|
"reasoning_channel_id": "U0123456789abcdef0123456789abcd",
|
||||||
"allow_from": []
|
"allow_from": []
|
||||||
},
|
},
|
||||||
"onebot": {
|
"onebot": {
|
||||||
|
|
@ -112,6 +121,7 @@
|
||||||
"access_token": "",
|
"access_token": "",
|
||||||
"reconnect_interval": 5,
|
"reconnect_interval": 5,
|
||||||
"group_trigger_prefix": [],
|
"group_trigger_prefix": [],
|
||||||
|
"reasoning_channel_id": "123456789",
|
||||||
"allow_from": []
|
"allow_from": []
|
||||||
},
|
},
|
||||||
"wecom": {
|
"wecom": {
|
||||||
|
|
@ -123,6 +133,7 @@
|
||||||
"webhook_host": "0.0.0.0",
|
"webhook_host": "0.0.0.0",
|
||||||
"webhook_port": 18793,
|
"webhook_port": 18793,
|
||||||
"webhook_path": "/webhook/wecom",
|
"webhook_path": "/webhook/wecom",
|
||||||
|
"reasoning_channel_id": "wecom-group-001",
|
||||||
"allow_from": [],
|
"allow_from": [],
|
||||||
"reply_timeout": 5
|
"reply_timeout": 5
|
||||||
},
|
},
|
||||||
|
|
@ -137,6 +148,7 @@
|
||||||
"webhook_host": "0.0.0.0",
|
"webhook_host": "0.0.0.0",
|
||||||
"webhook_port": 18792,
|
"webhook_port": 18792,
|
||||||
"webhook_path": "/webhook/wecom-app",
|
"webhook_path": "/webhook/wecom-app",
|
||||||
|
"reasoning_channel_id": "wecom-user-001",
|
||||||
"allow_from": [],
|
"allow_from": [],
|
||||||
"reply_timeout": 5
|
"reply_timeout": 5
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,25 @@ func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const telegramMaxMessageLength = 4096
|
||||||
|
|
||||||
|
|
||||||
|
func chunkString(s string, size int) []string {
|
||||||
|
if size <= 0 || s == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
runes := []rune(s)
|
||||||
|
chunks := make([]string, 0, (len(runes)+size-1)/size)
|
||||||
|
|
||||||
|
for i := 0; i < len(runes); i += size {
|
||||||
|
end := min(i + size, len(runes))
|
||||||
|
chunks = append(chunks, string(runes[i:end]))
|
||||||
|
}
|
||||||
|
|
||||||
|
return chunks
|
||||||
|
}
|
||||||
|
|
||||||
// registerSharedTools registers tools that are shared across all agents (web, message, spawn).
|
// registerSharedTools registers tools that are shared across all agents (web, message, spawn).
|
||||||
func registerSharedTools(
|
func registerSharedTools(
|
||||||
cfg *config.Config,
|
cfg *config.Config,
|
||||||
|
|
@ -466,6 +485,58 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt
|
||||||
return finalContent, nil
|
return finalContent, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (al *AgentLoop) targetReasoningChannelID(channelName string) (chatID string) {
|
||||||
|
var channels = al.cfg.Channels
|
||||||
|
|
||||||
|
switch channelName {
|
||||||
|
case "telegram":
|
||||||
|
return channels.Telegram.ReasoningChannelID
|
||||||
|
case "whatsapp":
|
||||||
|
return channels.WhatsApp.ReasoningChannelID
|
||||||
|
case "feishu":
|
||||||
|
return channels.Feishu.ReasoningChannelID
|
||||||
|
case "discord":
|
||||||
|
return channels.Discord.ReasoningChannelID
|
||||||
|
case "maixcam":
|
||||||
|
return channels.MaixCam.ReasoningChannelID
|
||||||
|
case "qq":
|
||||||
|
return channels.QQ.ReasoningChannelID
|
||||||
|
case "dingtalk":
|
||||||
|
return channels.DingTalk.ReasoningChannelID
|
||||||
|
case "slack":
|
||||||
|
return channels.Slack.ReasoningChannelID
|
||||||
|
case "line":
|
||||||
|
return channels.LINE.ReasoningChannelID
|
||||||
|
case "onebot":
|
||||||
|
return channels.OneBot.ReasoningChannelID
|
||||||
|
case "wecom":
|
||||||
|
return channels.WeCom.ReasoningChannelID
|
||||||
|
case "wecom_app":
|
||||||
|
return channels.WeComApp.ReasoningChannelID
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func (al *AgentLoop) handleReasoning(reasoningContent, channelName, channelID string) {
|
||||||
|
if reasoningContent == "" || channelName == "" || channelID == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
messages := []string{reasoningContent}
|
||||||
|
if channelName == "telegram" {
|
||||||
|
messages = chunkString(reasoningContent, telegramMaxMessageLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, message := range messages {
|
||||||
|
al.bus.PublishOutbound(bus.OutboundMessage{
|
||||||
|
Channel: channelName,
|
||||||
|
ChatID: channelID,
|
||||||
|
Content: message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// runLLMIteration executes the LLM call loop with tool handling.
|
// runLLMIteration executes the LLM call loop with tool handling.
|
||||||
func (al *AgentLoop) runLLMIteration(
|
func (al *AgentLoop) runLLMIteration(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
|
|
@ -521,6 +592,7 @@ func (al *AgentLoop) runLLMIteration(
|
||||||
return agent.Provider.Chat(ctx, messages, providerToolDefs, model, map[string]any{
|
return agent.Provider.Chat(ctx, messages, providerToolDefs, model, map[string]any{
|
||||||
"max_tokens": agent.MaxTokens,
|
"max_tokens": agent.MaxTokens,
|
||||||
"temperature": agent.Temperature,
|
"temperature": agent.Temperature,
|
||||||
|
"think": true,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
@ -589,6 +661,17 @@ func (al *AgentLoop) runLLMIteration(
|
||||||
})
|
})
|
||||||
return "", iteration, fmt.Errorf("LLM call failed after retries: %w", err)
|
return "", iteration, fmt.Errorf("LLM call failed after retries: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
go al.handleReasoning(response.Reasoning, opts.Channel , al.targetReasoningChannelID(opts.Channel))
|
||||||
|
// Log LLM response details
|
||||||
|
logger.InfoCF("agent", "LLM response",
|
||||||
|
map[string]any{
|
||||||
|
"agent_id": agent.ID,
|
||||||
|
"iteration": iteration,
|
||||||
|
"content_chars": len(response.Content),
|
||||||
|
"tool_calls": len(response.ToolCalls),
|
||||||
|
"reasoning": response.Reasoning,
|
||||||
|
})
|
||||||
|
|
||||||
// Check if no tool calls - we're done
|
// Check if no tool calls - we're done
|
||||||
if len(response.ToolCalls) == 0 {
|
if len(response.ToolCalls) == 0 {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/pkg/bus"
|
"github.com/sipeed/picoclaw/pkg/bus"
|
||||||
"github.com/sipeed/picoclaw/pkg/config"
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
|
|
@ -631,3 +632,184 @@ func TestAgentLoop_ContextExhaustionRetry(t *testing.T) {
|
||||||
t.Errorf("Expected history to be compressed (len < 8), got %d", len(finalHistory))
|
t.Errorf("Expected history to be compressed (len < 8), got %d", len(finalHistory))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestChunkString(t *testing.T) {
|
||||||
|
t.Run("returns nil for invalid size or empty input", func(t *testing.T) {
|
||||||
|
if got := chunkString("", 10); got != nil {
|
||||||
|
t.Fatalf("expected nil for empty input, got %v", got)
|
||||||
|
}
|
||||||
|
if got := chunkString("abc", 0); got != nil {
|
||||||
|
t.Fatalf("expected nil for size=0, got %v", got)
|
||||||
|
}
|
||||||
|
if got := chunkString("abc", -1); got != nil {
|
||||||
|
t.Fatalf("expected nil for size<0, got %v", got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("chunks by rune count and preserves utf8 validity", func(t *testing.T) {
|
||||||
|
in := "ab😀cd界x"
|
||||||
|
got := chunkString(in, 3)
|
||||||
|
want := []string{"ab😀", "cd界", "x"}
|
||||||
|
if len(got) != len(want) {
|
||||||
|
t.Fatalf("chunk count mismatch: got %d want %d (%v)", len(got), len(want), got)
|
||||||
|
}
|
||||||
|
for i := range want {
|
||||||
|
if got[i] != want[i] {
|
||||||
|
t.Fatalf("chunk[%d] mismatch: got %q want %q", i, got[i], want[i])
|
||||||
|
}
|
||||||
|
if !utf8.ValidString(got[i]) {
|
||||||
|
t.Fatalf("chunk[%d] is not valid utf8: %q", i, got[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTargetReasoningChannelID_AllChannels(t *testing.T) {
|
||||||
|
tmpDir, err := os.MkdirTemp("", "agent-test-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp dir: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
cfg := &config.Config{
|
||||||
|
Agents: config.AgentsConfig{
|
||||||
|
Defaults: config.AgentDefaults{
|
||||||
|
Workspace: tmpDir,
|
||||||
|
Model: "test-model",
|
||||||
|
MaxTokens: 4096,
|
||||||
|
MaxToolIterations: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Channels: config.ChannelsConfig{
|
||||||
|
WhatsApp: config.WhatsAppConfig{ReasoningChannelID: "rid-whatsapp"},
|
||||||
|
Telegram: config.TelegramConfig{ReasoningChannelID: "rid-telegram"},
|
||||||
|
Feishu: config.FeishuConfig{ReasoningChannelID: "rid-feishu"},
|
||||||
|
Discord: config.DiscordConfig{ReasoningChannelID: "rid-discord"},
|
||||||
|
MaixCam: config.MaixCamConfig{ReasoningChannelID: "rid-maixcam"},
|
||||||
|
QQ: config.QQConfig{ReasoningChannelID: "rid-qq"},
|
||||||
|
DingTalk: config.DingTalkConfig{ReasoningChannelID: "rid-dingtalk"},
|
||||||
|
Slack: config.SlackConfig{ReasoningChannelID: "rid-slack"},
|
||||||
|
LINE: config.LINEConfig{ReasoningChannelID: "rid-line"},
|
||||||
|
OneBot: config.OneBotConfig{ReasoningChannelID: "rid-onebot"},
|
||||||
|
WeCom: config.WeComConfig{ReasoningChannelID: "rid-wecom"},
|
||||||
|
WeComApp: config.WeComAppConfig{ReasoningChannelID: "rid-wecom-app"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
al := NewAgentLoop(cfg, bus.NewMessageBus(), &mockProvider{})
|
||||||
|
tests := []struct {
|
||||||
|
channel string
|
||||||
|
wantID string
|
||||||
|
}{
|
||||||
|
{channel: "whatsapp", wantID: "rid-whatsapp"},
|
||||||
|
{channel: "telegram", wantID: "rid-telegram"},
|
||||||
|
{channel: "feishu", wantID: "rid-feishu"},
|
||||||
|
{channel: "discord", wantID: "rid-discord"},
|
||||||
|
{channel: "maixcam", wantID: "rid-maixcam"},
|
||||||
|
{channel: "qq", wantID: "rid-qq"},
|
||||||
|
{channel: "dingtalk", wantID: "rid-dingtalk"},
|
||||||
|
{channel: "slack", wantID: "rid-slack"},
|
||||||
|
{channel: "line", wantID: "rid-line"},
|
||||||
|
{channel: "onebot", wantID: "rid-onebot"},
|
||||||
|
{channel: "wecom", wantID: "rid-wecom"},
|
||||||
|
{channel: "wecom_app", wantID: "rid-wecom-app"},
|
||||||
|
{channel: "unknown", wantID: ""},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.channel, func(t *testing.T) {
|
||||||
|
got := al.targetReasoningChannelID(tt.channel)
|
||||||
|
if got != tt.wantID {
|
||||||
|
t.Fatalf("targetReasoningChannelID(%q) = %q, want %q", tt.channel, got, tt.wantID)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleReasoning(t *testing.T) {
|
||||||
|
newLoop := func(t *testing.T) (*AgentLoop, *bus.MessageBus) {
|
||||||
|
t.Helper()
|
||||||
|
tmpDir, err := os.MkdirTemp("", "agent-test-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp dir: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { _ = os.RemoveAll(tmpDir) })
|
||||||
|
cfg := &config.Config{
|
||||||
|
Agents: config.AgentsConfig{
|
||||||
|
Defaults: config.AgentDefaults{
|
||||||
|
Workspace: tmpDir,
|
||||||
|
Model: "test-model",
|
||||||
|
MaxTokens: 4096,
|
||||||
|
MaxToolIterations: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
msgBus := bus.NewMessageBus()
|
||||||
|
return NewAgentLoop(cfg, msgBus, &mockProvider{}), msgBus
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("skips when any required field is empty", func(t *testing.T) {
|
||||||
|
al, msgBus := newLoop(t)
|
||||||
|
al.handleReasoning("reasoning", "telegram", "")
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
|
||||||
|
defer cancel()
|
||||||
|
if msg, ok := msgBus.SubscribeOutbound(ctx); ok {
|
||||||
|
t.Fatalf("expected no outbound message, got %+v", msg)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("publishes one message for non telegram", func(t *testing.T) {
|
||||||
|
al, msgBus := newLoop(t)
|
||||||
|
al.handleReasoning("hello reasoning", "slack", "channel-1")
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
|
||||||
|
defer cancel()
|
||||||
|
msg, ok := msgBus.SubscribeOutbound(ctx)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected an outbound message")
|
||||||
|
}
|
||||||
|
if msg.Channel != "slack" || msg.ChatID != "channel-1" || msg.Content != "hello reasoning" {
|
||||||
|
t.Fatalf("unexpected outbound message: %+v", msg)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("chunks telegram messages", func(t *testing.T) {
|
||||||
|
al, msgBus := newLoop(t)
|
||||||
|
large := make([]rune, telegramMaxMessageLength+5)
|
||||||
|
for i := range large {
|
||||||
|
large[i] = '界'
|
||||||
|
}
|
||||||
|
largeReasoning := string(large)
|
||||||
|
al.handleReasoning(largeReasoning, "telegram", "tg-chat")
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
|
||||||
|
defer cancel()
|
||||||
|
msg1, ok := msgBus.SubscribeOutbound(ctx)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected first outbound message")
|
||||||
|
}
|
||||||
|
msg2, ok := msgBus.SubscribeOutbound(ctx)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected second outbound message")
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg1.Channel != "telegram" || msg2.Channel != "telegram" {
|
||||||
|
t.Fatalf("expected telegram channel messages, got %+v and %+v", msg1, msg2)
|
||||||
|
}
|
||||||
|
if msg1.ChatID != "tg-chat" || msg2.ChatID != "tg-chat" {
|
||||||
|
t.Fatalf("expected chatID tg-chat, got %+v and %+v", msg1, msg2)
|
||||||
|
}
|
||||||
|
|
||||||
|
gotCombined := msg1.Content + msg2.Content
|
||||||
|
if gotCombined != largeReasoning {
|
||||||
|
t.Fatalf("chunked content mismatch: got len=%d want len=%d", len(gotCombined), len(largeReasoning))
|
||||||
|
}
|
||||||
|
if len([]rune(msg1.Content)) != telegramMaxMessageLength {
|
||||||
|
t.Fatalf("first chunk rune length = %d, want %d", len([]rune(msg1.Content)), telegramMaxMessageLength)
|
||||||
|
}
|
||||||
|
if len([]rune(msg2.Content)) != 5 {
|
||||||
|
t.Fatalf("second chunk rune length = %d, want 5", len([]rune(msg2.Content)))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -193,11 +193,11 @@ type ChannelsConfig struct {
|
||||||
WeCom WeComConfig `json:"wecom"`
|
WeCom WeComConfig `json:"wecom"`
|
||||||
WeComApp WeComAppConfig `json:"wecom_app"`
|
WeComApp WeComAppConfig `json:"wecom_app"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type WhatsAppConfig struct {
|
type WhatsAppConfig struct {
|
||||||
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_WHATSAPP_ENABLED"`
|
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_WHATSAPP_ENABLED"`
|
||||||
BridgeURL string `json:"bridge_url" env:"PICOCLAW_CHANNELS_WHATSAPP_BRIDGE_URL"`
|
BridgeURL string `json:"bridge_url" env:"PICOCLAW_CHANNELS_WHATSAPP_BRIDGE_URL"`
|
||||||
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_WHATSAPP_ALLOW_FROM"`
|
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_WHATSAPP_ALLOW_FROM"`
|
||||||
|
ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_WHATSAPP_REASONING_CHANNEL_ID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TelegramConfig struct {
|
type TelegramConfig struct {
|
||||||
|
|
@ -205,6 +205,7 @@ type TelegramConfig struct {
|
||||||
Token string `json:"token" env:"PICOCLAW_CHANNELS_TELEGRAM_TOKEN"`
|
Token string `json:"token" env:"PICOCLAW_CHANNELS_TELEGRAM_TOKEN"`
|
||||||
Proxy string `json:"proxy" env:"PICOCLAW_CHANNELS_TELEGRAM_PROXY"`
|
Proxy string `json:"proxy" env:"PICOCLAW_CHANNELS_TELEGRAM_PROXY"`
|
||||||
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_TELEGRAM_ALLOW_FROM"`
|
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_TELEGRAM_ALLOW_FROM"`
|
||||||
|
ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_TELEGRAM_REASONING_CHANNEL_ID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FeishuConfig struct {
|
type FeishuConfig struct {
|
||||||
|
|
@ -214,6 +215,7 @@ type FeishuConfig struct {
|
||||||
EncryptKey string `json:"encrypt_key" env:"PICOCLAW_CHANNELS_FEISHU_ENCRYPT_KEY"`
|
EncryptKey string `json:"encrypt_key" env:"PICOCLAW_CHANNELS_FEISHU_ENCRYPT_KEY"`
|
||||||
VerificationToken string `json:"verification_token" env:"PICOCLAW_CHANNELS_FEISHU_VERIFICATION_TOKEN"`
|
VerificationToken string `json:"verification_token" env:"PICOCLAW_CHANNELS_FEISHU_VERIFICATION_TOKEN"`
|
||||||
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_FEISHU_ALLOW_FROM"`
|
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_FEISHU_ALLOW_FROM"`
|
||||||
|
ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_FEISHU_REASONING_CHANNEL_ID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DiscordConfig struct {
|
type DiscordConfig struct {
|
||||||
|
|
@ -221,6 +223,7 @@ type DiscordConfig struct {
|
||||||
Token string `json:"token" env:"PICOCLAW_CHANNELS_DISCORD_TOKEN"`
|
Token string `json:"token" env:"PICOCLAW_CHANNELS_DISCORD_TOKEN"`
|
||||||
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_DISCORD_ALLOW_FROM"`
|
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_DISCORD_ALLOW_FROM"`
|
||||||
MentionOnly bool `json:"mention_only" env:"PICOCLAW_CHANNELS_DISCORD_MENTION_ONLY"`
|
MentionOnly bool `json:"mention_only" env:"PICOCLAW_CHANNELS_DISCORD_MENTION_ONLY"`
|
||||||
|
ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_DISCORD_REASONING_CHANNEL_ID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type MaixCamConfig struct {
|
type MaixCamConfig struct {
|
||||||
|
|
@ -228,6 +231,7 @@ type MaixCamConfig struct {
|
||||||
Host string `json:"host" env:"PICOCLAW_CHANNELS_MAIXCAM_HOST"`
|
Host string `json:"host" env:"PICOCLAW_CHANNELS_MAIXCAM_HOST"`
|
||||||
Port int `json:"port" env:"PICOCLAW_CHANNELS_MAIXCAM_PORT"`
|
Port int `json:"port" env:"PICOCLAW_CHANNELS_MAIXCAM_PORT"`
|
||||||
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_MAIXCAM_ALLOW_FROM"`
|
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_MAIXCAM_ALLOW_FROM"`
|
||||||
|
ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_MAIXCAM_REASONING_CHANNEL_ID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type QQConfig struct {
|
type QQConfig struct {
|
||||||
|
|
@ -235,6 +239,7 @@ type QQConfig struct {
|
||||||
AppID string `json:"app_id" env:"PICOCLAW_CHANNELS_QQ_APP_ID"`
|
AppID string `json:"app_id" env:"PICOCLAW_CHANNELS_QQ_APP_ID"`
|
||||||
AppSecret string `json:"app_secret" env:"PICOCLAW_CHANNELS_QQ_APP_SECRET"`
|
AppSecret string `json:"app_secret" env:"PICOCLAW_CHANNELS_QQ_APP_SECRET"`
|
||||||
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_QQ_ALLOW_FROM"`
|
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_QQ_ALLOW_FROM"`
|
||||||
|
ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_QQ_REASONING_CHANNEL_ID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DingTalkConfig struct {
|
type DingTalkConfig struct {
|
||||||
|
|
@ -242,6 +247,7 @@ type DingTalkConfig struct {
|
||||||
ClientID string `json:"client_id" env:"PICOCLAW_CHANNELS_DINGTALK_CLIENT_ID"`
|
ClientID string `json:"client_id" env:"PICOCLAW_CHANNELS_DINGTALK_CLIENT_ID"`
|
||||||
ClientSecret string `json:"client_secret" env:"PICOCLAW_CHANNELS_DINGTALK_CLIENT_SECRET"`
|
ClientSecret string `json:"client_secret" env:"PICOCLAW_CHANNELS_DINGTALK_CLIENT_SECRET"`
|
||||||
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_DINGTALK_ALLOW_FROM"`
|
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_DINGTALK_ALLOW_FROM"`
|
||||||
|
ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_DINGTALK_REASONING_CHANNEL_ID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SlackConfig struct {
|
type SlackConfig struct {
|
||||||
|
|
@ -249,6 +255,7 @@ type SlackConfig struct {
|
||||||
BotToken string `json:"bot_token" env:"PICOCLAW_CHANNELS_SLACK_BOT_TOKEN"`
|
BotToken string `json:"bot_token" env:"PICOCLAW_CHANNELS_SLACK_BOT_TOKEN"`
|
||||||
AppToken string `json:"app_token" env:"PICOCLAW_CHANNELS_SLACK_APP_TOKEN"`
|
AppToken string `json:"app_token" env:"PICOCLAW_CHANNELS_SLACK_APP_TOKEN"`
|
||||||
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_SLACK_ALLOW_FROM"`
|
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_SLACK_ALLOW_FROM"`
|
||||||
|
ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_SLACK_REASONING_CHANNEL_ID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LINEConfig struct {
|
type LINEConfig struct {
|
||||||
|
|
@ -259,6 +266,7 @@ type LINEConfig struct {
|
||||||
WebhookPort int `json:"webhook_port" env:"PICOCLAW_CHANNELS_LINE_WEBHOOK_PORT"`
|
WebhookPort int `json:"webhook_port" env:"PICOCLAW_CHANNELS_LINE_WEBHOOK_PORT"`
|
||||||
WebhookPath string `json:"webhook_path" env:"PICOCLAW_CHANNELS_LINE_WEBHOOK_PATH"`
|
WebhookPath string `json:"webhook_path" env:"PICOCLAW_CHANNELS_LINE_WEBHOOK_PATH"`
|
||||||
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_LINE_ALLOW_FROM"`
|
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_LINE_ALLOW_FROM"`
|
||||||
|
ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_LINE_REASONING_CHANNEL_ID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OneBotConfig struct {
|
type OneBotConfig struct {
|
||||||
|
|
@ -268,6 +276,7 @@ type OneBotConfig struct {
|
||||||
ReconnectInterval int `json:"reconnect_interval" env:"PICOCLAW_CHANNELS_ONEBOT_RECONNECT_INTERVAL"`
|
ReconnectInterval int `json:"reconnect_interval" env:"PICOCLAW_CHANNELS_ONEBOT_RECONNECT_INTERVAL"`
|
||||||
GroupTriggerPrefix []string `json:"group_trigger_prefix" env:"PICOCLAW_CHANNELS_ONEBOT_GROUP_TRIGGER_PREFIX"`
|
GroupTriggerPrefix []string `json:"group_trigger_prefix" env:"PICOCLAW_CHANNELS_ONEBOT_GROUP_TRIGGER_PREFIX"`
|
||||||
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_ONEBOT_ALLOW_FROM"`
|
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_ONEBOT_ALLOW_FROM"`
|
||||||
|
ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_ONEBOT_REASONING_CHANNEL_ID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type WeComConfig struct {
|
type WeComConfig struct {
|
||||||
|
|
@ -280,6 +289,7 @@ type WeComConfig struct {
|
||||||
WebhookPath string `json:"webhook_path" env:"PICOCLAW_CHANNELS_WECOM_WEBHOOK_PATH"`
|
WebhookPath string `json:"webhook_path" env:"PICOCLAW_CHANNELS_WECOM_WEBHOOK_PATH"`
|
||||||
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_WECOM_ALLOW_FROM"`
|
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_WECOM_ALLOW_FROM"`
|
||||||
ReplyTimeout int `json:"reply_timeout" env:"PICOCLAW_CHANNELS_WECOM_REPLY_TIMEOUT"`
|
ReplyTimeout int `json:"reply_timeout" env:"PICOCLAW_CHANNELS_WECOM_REPLY_TIMEOUT"`
|
||||||
|
ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_WECOM_REASONING_CHANNEL_ID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type WeComAppConfig struct {
|
type WeComAppConfig struct {
|
||||||
|
|
@ -294,6 +304,7 @@ type WeComAppConfig struct {
|
||||||
WebhookPath string `json:"webhook_path" env:"PICOCLAW_CHANNELS_WECOM_APP_WEBHOOK_PATH"`
|
WebhookPath string `json:"webhook_path" env:"PICOCLAW_CHANNELS_WECOM_APP_WEBHOOK_PATH"`
|
||||||
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_WECOM_APP_ALLOW_FROM"`
|
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_WECOM_APP_ALLOW_FROM"`
|
||||||
ReplyTimeout int `json:"reply_timeout" env:"PICOCLAW_CHANNELS_WECOM_APP_REPLY_TIMEOUT"`
|
ReplyTimeout int `json:"reply_timeout" env:"PICOCLAW_CHANNELS_WECOM_APP_REPLY_TIMEOUT"`
|
||||||
|
ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_WECOM_APP_REASONING_CHANNEL_ID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type HeartbeatConfig struct {
|
type HeartbeatConfig struct {
|
||||||
|
|
|
||||||
|
|
@ -148,6 +148,13 @@ func parseResponse(body []byte) (*LLMResponse, error) {
|
||||||
var apiResponse struct {
|
var apiResponse struct {
|
||||||
Choices []struct {
|
Choices []struct {
|
||||||
Message struct {
|
Message struct {
|
||||||
|
Reasoning string `json:"reasoning"`
|
||||||
|
ReasoningDetails []struct {
|
||||||
|
Format string `json:"format"`
|
||||||
|
Index int `json:"index"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
} `json:"reasoning_details"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
ToolCalls []struct {
|
ToolCalls []struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
|
|
@ -221,6 +228,8 @@ func parseResponse(body []byte) (*LLMResponse, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return &LLMResponse{
|
return &LLMResponse{
|
||||||
|
Reasoning: choice.Message.Reasoning,
|
||||||
|
ReasoningDetails: choice.Message.ReasoningDetails,
|
||||||
Content: choice.Message.Content,
|
Content: choice.Message.Content,
|
||||||
ToolCalls: toolCalls,
|
ToolCalls: toolCalls,
|
||||||
FinishReason: choice.FinishReason,
|
FinishReason: choice.FinishReason,
|
||||||
|
|
|
||||||
|
|
@ -25,12 +25,18 @@ type FunctionCall struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type LLMResponse struct {
|
type LLMResponse struct {
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
|
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
|
||||||
FinishReason string `json:"finish_reason"`
|
FinishReason string `json:"finish_reason"`
|
||||||
Usage *UsageInfo `json:"usage,omitempty"`
|
Usage *UsageInfo `json:"usage,omitempty"`
|
||||||
|
Reasoning string `json:"reasoning"`
|
||||||
|
ReasoningDetails []struct {
|
||||||
|
Format string `json:"format"`
|
||||||
|
Index int `json:"index"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
} `json:"reasoning_details"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UsageInfo struct {
|
type UsageInfo struct {
|
||||||
PromptTokens int `json:"prompt_tokens"`
|
PromptTokens int `json:"prompt_tokens"`
|
||||||
CompletionTokens int `json:"completion_tokens"`
|
CompletionTokens int `json:"completion_tokens"`
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue