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,
|
||||
"token": "YOUR_TELEGRAM_BOT_TOKEN",
|
||||
"proxy": "",
|
||||
"reasoning_channel_id": "-1001234567890",
|
||||
"allow_from": [
|
||||
"YOUR_USER_ID"
|
||||
]
|
||||
|
|
@ -57,6 +58,7 @@
|
|||
"discord": {
|
||||
"enabled": false,
|
||||
"token": "YOUR_DISCORD_BOT_TOKEN",
|
||||
"reasoning_channel_id": "123456789012345678",
|
||||
"allow_from": [],
|
||||
"mention_only": false
|
||||
},
|
||||
|
|
@ -64,17 +66,20 @@
|
|||
"enabled": false,
|
||||
"app_id": "YOUR_QQ_APP_ID",
|
||||
"app_secret": "YOUR_QQ_APP_SECRET",
|
||||
"reasoning_channel_id": "group-123456",
|
||||
"allow_from": []
|
||||
},
|
||||
"maixcam": {
|
||||
"enabled": false,
|
||||
"host": "0.0.0.0",
|
||||
"port": 18790,
|
||||
"reasoning_channel_id": "maixcam-room-1",
|
||||
"allow_from": []
|
||||
},
|
||||
"whatsapp": {
|
||||
"enabled": false,
|
||||
"bridge_url": "ws://localhost:3001",
|
||||
"reasoning_channel_id": "12025550123@c.us",
|
||||
"allow_from": []
|
||||
},
|
||||
"feishu": {
|
||||
|
|
@ -83,18 +88,21 @@
|
|||
"app_secret": "",
|
||||
"encrypt_key": "",
|
||||
"verification_token": "",
|
||||
"reasoning_channel_id": "oc_reasoning_chat_id",
|
||||
"allow_from": []
|
||||
},
|
||||
"dingtalk": {
|
||||
"enabled": false,
|
||||
"client_id": "YOUR_CLIENT_ID",
|
||||
"client_secret": "YOUR_CLIENT_SECRET",
|
||||
"reasoning_channel_id": "cid_reasoning_group",
|
||||
"allow_from": []
|
||||
},
|
||||
"slack": {
|
||||
"enabled": false,
|
||||
"bot_token": "xoxb-YOUR-BOT-TOKEN",
|
||||
"app_token": "xapp-YOUR-APP-TOKEN",
|
||||
"reasoning_channel_id": "C0123456789",
|
||||
"allow_from": []
|
||||
},
|
||||
"line": {
|
||||
|
|
@ -104,6 +112,7 @@
|
|||
"webhook_host": "0.0.0.0",
|
||||
"webhook_port": 18791,
|
||||
"webhook_path": "/webhook/line",
|
||||
"reasoning_channel_id": "U0123456789abcdef0123456789abcd",
|
||||
"allow_from": []
|
||||
},
|
||||
"onebot": {
|
||||
|
|
@ -112,6 +121,7 @@
|
|||
"access_token": "",
|
||||
"reconnect_interval": 5,
|
||||
"group_trigger_prefix": [],
|
||||
"reasoning_channel_id": "123456789",
|
||||
"allow_from": []
|
||||
},
|
||||
"wecom": {
|
||||
|
|
@ -123,6 +133,7 @@
|
|||
"webhook_host": "0.0.0.0",
|
||||
"webhook_port": 18793,
|
||||
"webhook_path": "/webhook/wecom",
|
||||
"reasoning_channel_id": "wecom-group-001",
|
||||
"allow_from": [],
|
||||
"reply_timeout": 5
|
||||
},
|
||||
|
|
@ -137,6 +148,7 @@
|
|||
"webhook_host": "0.0.0.0",
|
||||
"webhook_port": 18792,
|
||||
"webhook_path": "/webhook/wecom-app",
|
||||
"reasoning_channel_id": "wecom-user-001",
|
||||
"allow_from": [],
|
||||
"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).
|
||||
func registerSharedTools(
|
||||
cfg *config.Config,
|
||||
|
|
@ -466,6 +485,58 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt
|
|||
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.
|
||||
func (al *AgentLoop) runLLMIteration(
|
||||
ctx context.Context,
|
||||
|
|
@ -521,6 +592,7 @@ func (al *AgentLoop) runLLMIteration(
|
|||
return agent.Provider.Chat(ctx, messages, providerToolDefs, model, map[string]any{
|
||||
"max_tokens": agent.MaxTokens,
|
||||
"temperature": agent.Temperature,
|
||||
"think": true,
|
||||
})
|
||||
},
|
||||
)
|
||||
|
|
@ -589,6 +661,17 @@ func (al *AgentLoop) runLLMIteration(
|
|||
})
|
||||
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
|
||||
if len(response.ToolCalls) == 0 {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/bus"
|
||||
"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))
|
||||
}
|
||||
}
|
||||
|
||||
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"`
|
||||
WeComApp WeComAppConfig `json:"wecom_app"`
|
||||
}
|
||||
|
||||
type WhatsAppConfig struct {
|
||||
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_WHATSAPP_ENABLED"`
|
||||
BridgeURL string `json:"bridge_url" env:"PICOCLAW_CHANNELS_WHATSAPP_BRIDGE_URL"`
|
||||
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 {
|
||||
|
|
@ -205,6 +205,7 @@ type TelegramConfig struct {
|
|||
Token string `json:"token" env:"PICOCLAW_CHANNELS_TELEGRAM_TOKEN"`
|
||||
Proxy string `json:"proxy" env:"PICOCLAW_CHANNELS_TELEGRAM_PROXY"`
|
||||
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 {
|
||||
|
|
@ -214,6 +215,7 @@ type FeishuConfig struct {
|
|||
EncryptKey string `json:"encrypt_key" env:"PICOCLAW_CHANNELS_FEISHU_ENCRYPT_KEY"`
|
||||
VerificationToken string `json:"verification_token" env:"PICOCLAW_CHANNELS_FEISHU_VERIFICATION_TOKEN"`
|
||||
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 {
|
||||
|
|
@ -221,6 +223,7 @@ type DiscordConfig struct {
|
|||
Token string `json:"token" env:"PICOCLAW_CHANNELS_DISCORD_TOKEN"`
|
||||
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_DISCORD_ALLOW_FROM"`
|
||||
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 {
|
||||
|
|
@ -228,6 +231,7 @@ type MaixCamConfig struct {
|
|||
Host string `json:"host" env:"PICOCLAW_CHANNELS_MAIXCAM_HOST"`
|
||||
Port int `json:"port" env:"PICOCLAW_CHANNELS_MAIXCAM_PORT"`
|
||||
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 {
|
||||
|
|
@ -235,6 +239,7 @@ type QQConfig struct {
|
|||
AppID string `json:"app_id" env:"PICOCLAW_CHANNELS_QQ_APP_ID"`
|
||||
AppSecret string `json:"app_secret" env:"PICOCLAW_CHANNELS_QQ_APP_SECRET"`
|
||||
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 {
|
||||
|
|
@ -242,6 +247,7 @@ type DingTalkConfig struct {
|
|||
ClientID string `json:"client_id" env:"PICOCLAW_CHANNELS_DINGTALK_CLIENT_ID"`
|
||||
ClientSecret string `json:"client_secret" env:"PICOCLAW_CHANNELS_DINGTALK_CLIENT_SECRET"`
|
||||
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 {
|
||||
|
|
@ -249,6 +255,7 @@ type SlackConfig struct {
|
|||
BotToken string `json:"bot_token" env:"PICOCLAW_CHANNELS_SLACK_BOT_TOKEN"`
|
||||
AppToken string `json:"app_token" env:"PICOCLAW_CHANNELS_SLACK_APP_TOKEN"`
|
||||
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 {
|
||||
|
|
@ -259,6 +266,7 @@ type LINEConfig struct {
|
|||
WebhookPort int `json:"webhook_port" env:"PICOCLAW_CHANNELS_LINE_WEBHOOK_PORT"`
|
||||
WebhookPath string `json:"webhook_path" env:"PICOCLAW_CHANNELS_LINE_WEBHOOK_PATH"`
|
||||
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 {
|
||||
|
|
@ -268,6 +276,7 @@ type OneBotConfig struct {
|
|||
ReconnectInterval int `json:"reconnect_interval" env:"PICOCLAW_CHANNELS_ONEBOT_RECONNECT_INTERVAL"`
|
||||
GroupTriggerPrefix []string `json:"group_trigger_prefix" env:"PICOCLAW_CHANNELS_ONEBOT_GROUP_TRIGGER_PREFIX"`
|
||||
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 {
|
||||
|
|
@ -280,6 +289,7 @@ type WeComConfig struct {
|
|||
WebhookPath string `json:"webhook_path" env:"PICOCLAW_CHANNELS_WECOM_WEBHOOK_PATH"`
|
||||
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_WECOM_ALLOW_FROM"`
|
||||
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 {
|
||||
|
|
@ -294,6 +304,7 @@ type WeComAppConfig struct {
|
|||
WebhookPath string `json:"webhook_path" env:"PICOCLAW_CHANNELS_WECOM_APP_WEBHOOK_PATH"`
|
||||
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_WECOM_APP_ALLOW_FROM"`
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -148,6 +148,13 @@ func parseResponse(body []byte) (*LLMResponse, error) {
|
|||
var apiResponse struct {
|
||||
Choices []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"`
|
||||
ToolCalls []struct {
|
||||
ID string `json:"id"`
|
||||
|
|
@ -221,6 +228,8 @@ func parseResponse(body []byte) (*LLMResponse, error) {
|
|||
}
|
||||
|
||||
return &LLMResponse{
|
||||
Reasoning: choice.Message.Reasoning,
|
||||
ReasoningDetails: choice.Message.ReasoningDetails,
|
||||
Content: choice.Message.Content,
|
||||
ToolCalls: toolCalls,
|
||||
FinishReason: choice.FinishReason,
|
||||
|
|
|
|||
|
|
@ -25,12 +25,18 @@ type FunctionCall struct {
|
|||
}
|
||||
|
||||
type LLMResponse struct {
|
||||
Content string `json:"content"`
|
||||
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
|
||||
FinishReason string `json:"finish_reason"`
|
||||
Usage *UsageInfo `json:"usage,omitempty"`
|
||||
Content string `json:"content"`
|
||||
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
|
||||
FinishReason string `json:"finish_reason"`
|
||||
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 {
|
||||
PromptTokens int `json:"prompt_tokens"`
|
||||
CompletionTokens int `json:"completion_tokens"`
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue