-
Notifications
You must be signed in to change notification settings - Fork 3
feat(chat): add langchaingo agent execution path for chat module #710
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
09e5e11
832105d
1dfce60
2208783
16da9ff
6ebda7a
1485fcf
565414e
e05a44d
ee41c88
2e31396
edc1e15
e068049
9c1c984
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -120,6 +120,19 @@ type promptData struct { | |||||||||||||||||
| BotUsername string // 添加 Bot 用户名字段 | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| func extractInputFromMessage(msg *tb.Message, trigger *config.ChatTrigger) string { | ||||||||||||||||||
| input := msg.Text | ||||||||||||||||||
| if input == "" { | ||||||||||||||||||
| input = msg.Caption | ||||||||||||||||||
| } | ||||||||||||||||||
| if trigger.Command != "" { | ||||||||||||||||||
| if _, text, err := entities.CommandFromText(input, 0); err == nil { | ||||||||||||||||||
| input = text | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
| return input | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| // Chat 处理聊天请求 | ||||||||||||||||||
| func Chat(ctx tb.Context, v2 *config.ChatConfigSingle, trigger *config.ChatTrigger) error { | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
@@ -137,16 +150,7 @@ func Chat(ctx tb.Context, v2 *config.ChatConfigSingle, trigger *config.ChatTrigg | |||||||||||||||||
| return nil | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| input := ctx.Message().Text | ||||||||||||||||||
| if input == "" { | ||||||||||||||||||
| input = ctx.Message().Caption | ||||||||||||||||||
| } | ||||||||||||||||||
| if trigger.Command != "" { | ||||||||||||||||||
| _, text, err := entities.CommandFromText(input, 0) | ||||||||||||||||||
| if err != nil { | ||||||||||||||||||
| input = text | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
| input := extractInputFromMessage(ctx.Message(), trigger) | ||||||||||||||||||
|
|
||||||||||||||||||
| // if gacha, reply and not send placeholder | ||||||||||||||||||
| isGacha := trigger.Gacha > 0 | ||||||||||||||||||
|
|
@@ -206,6 +210,7 @@ func Chat(ctx tb.Context, v2 *config.ChatConfigSingle, trigger *config.ChatTrigg | |||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| multiPartContent := false | ||||||||||||||||||
| var imageDataURL string | ||||||||||||||||||
| var contents []openai.ChatMessagePart | ||||||||||||||||||
| if v2.Model.Features.Image && v2.Features.Image { | ||||||||||||||||||
| // TODO handle multi photos album | ||||||||||||||||||
|
|
@@ -244,11 +249,12 @@ func Chat(ctx tb.Context, v2 *config.ChatConfigSingle, trigger *config.ChatTrigg | |||||||||||||||||
| base64Img := []byte("data:image/jpeg;base64,") | ||||||||||||||||||
| base64Img = base64.StdEncoding.AppendEncode(base64Img, buf.Bytes()) | ||||||||||||||||||
| log.Info("encoded base64 image data url size", zap.Int("size", len(base64Img))) | ||||||||||||||||||
| imageDataURL = string(base64Img) | ||||||||||||||||||
| contents = append(contents, | ||||||||||||||||||
| openai.ChatMessagePart{ | ||||||||||||||||||
| Type: openai.ChatMessagePartTypeImageURL, | ||||||||||||||||||
| ImageURL: &openai.ChatMessageImageURL{ | ||||||||||||||||||
| URL: string(base64Img), | ||||||||||||||||||
| URL: imageDataURL, | ||||||||||||||||||
| }, | ||||||||||||||||||
| }, | ||||||||||||||||||
| openai.ChatMessagePart{ | ||||||||||||||||||
|
|
@@ -275,8 +281,6 @@ final: | |||||||||||||||||
|
|
||||||||||||||||||
| // zap.L().Debug("Chat context messages", zap.Any("messages", messages)) | ||||||||||||||||||
|
|
||||||||||||||||||
| client := clients[v2.Model.Name] | ||||||||||||||||||
|
|
||||||||||||||||||
| // 处理place_holder功能 | ||||||||||||||||||
| var placeholderMsg *tb.Message | ||||||||||||||||||
| switch { | ||||||||||||||||||
|
|
@@ -288,7 +292,6 @@ final: | |||||||||||||||||
| placeholderMsg, placeHolderErr = ctx.Bot().Reply(ctx.Message(), v2.PlaceHolder, tb.ModeMarkdownV2) | ||||||||||||||||||
| if placeHolderErr != nil { | ||||||||||||||||||
| log.Error("Failed to send placeholder message", zap.Error(placeHolderErr)) | ||||||||||||||||||
| // 如果发送placeholder失败,继续正常流程,不使用placeholder功能 | ||||||||||||||||||
| } | ||||||||||||||||||
| default: | ||||||||||||||||||
| err = ctx.Bot().Notify(ctx.Chat(), tb.Typing) | ||||||||||||||||||
|
|
@@ -300,24 +303,91 @@ final: | |||||||||||||||||
| chatCtx, cancel := context.WithTimeout(context.Background(), v2.GetTimeout()) | ||||||||||||||||||
| defer cancel() | ||||||||||||||||||
|
|
||||||||||||||||||
| // Use agent-based execution path if enabled | ||||||||||||||||||
| if v2.Agent.Enabled { | ||||||||||||||||||
| return chatWithAgent(chatCtx, ctx, v2, systemPrompt, promptBuf.String(), | ||||||||||||||||||
| multiPartContent, imageDataURL, placeholderMsg, messages) | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| // Legacy execution path using direct OpenAI SDK | ||||||||||||||||||
| return chatWithLegacy(chatCtx, ctx, v2, messages, placeholderMsg) | ||||||||||||||||||
|
|
||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| // chatWithAgent executes the chat using the langchaingo agent-based path. | ||||||||||||||||||
| func chatWithAgent( | ||||||||||||||||||
| chatCtx context.Context, ctx tb.Context, v2 *config.ChatConfigSingle, | ||||||||||||||||||
| systemPrompt string, userPrompt string, | ||||||||||||||||||
| multiPartContent bool, imageURL string, | ||||||||||||||||||
| placeholderMsg *tb.Message, | ||||||||||||||||||
| messages []openai.ChatCompletionMessage, | ||||||||||||||||||
| ) error { | ||||||||||||||||||
| model := getLangchainModel(v2.Model.Name) | ||||||||||||||||||
| if model == nil { | ||||||||||||||||||
| log.Error("langchain model not found, falling back to legacy path", zap.String("model", v2.Model.Name)) | ||||||||||||||||||
| return chatWithLegacy(chatCtx, ctx, v2, messages, placeholderMsg) | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| lcMessages := convertToLangchainMessages(systemPrompt, userPrompt, multiPartContent, imageURL) | ||||||||||||||||||
|
|
||||||||||||||||||
| ap := newAgentProcessor(chatCtx, ctx, model, v2, lcMessages, placeholderMsg) | ||||||||||||||||||
| response, err := ap.run() | ||||||||||||||||||
| if err != nil { | ||||||||||||||||||
| log.Error("agent execution failed", zap.Error(err)) | ||||||||||||||||||
| if placeholderMsg != nil { | ||||||||||||||||||
| _, editErr := util.EditMessageWithError(placeholderMsg, v2.GetErrorMessage(), tb.ModeMarkdownV2) | ||||||||||||||||||
| if editErr != nil { | ||||||||||||||||||
| log.Error("Failed to edit placeholder with error", zap.Error(editErr)) | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
| return err | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| // Process outgoing filters | ||||||||||||||||||
| for _, filterConfig := range v2.Filters.Filters { | ||||||||||||||||||
| filter := createFilter(&filterConfig) | ||||||||||||||||||
| if filter == nil { | ||||||||||||||||||
| continue | ||||||||||||||||||
| } | ||||||||||||||||||
| response = filter.ProcessOutgoing(response, ctx, v2) | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
|
||||||||||||||||||
| // Ensure the filtered response is reflected in the final user-visible message. | |
| if placeholderMsg != nil { | |
| if _, err := util.EditMessageWithError(placeholderMsg, response, tb.ModeMarkdownV2); err != nil { | |
| log.Error("failed to edit placeholder with filtered agent response", zap.Error(err)) | |
| } | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note: the outgoing filter behavior is consistent with the legacy path (chatWithLegacy), which also applies ProcessOutgoing after the stream processor has already sent/edited the message. Both paths apply filters to the returned response text after it's been sent. If we want filters to modify what the user sees, both paths would need refactoring together — keeping them consistent for now in 6ebda7a.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| package chat | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "csust-got/config" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
| tb "gopkg.in/telebot.v3" | ||
| ) | ||
|
|
||
| func TestExtractInputFromMessage_RemovesCommandPrefix(t *testing.T) { | ||
| trigger := &config.ChatTrigger{Command: "think"} | ||
| tests := []struct { | ||
| name string | ||
| msg *tb.Message | ||
| want string | ||
| }{ | ||
| { | ||
| name: "plain command prefix stripped", | ||
| msg: &tb.Message{Text: "/think hello world"}, | ||
| want: "hello world", | ||
| }, | ||
| { | ||
| name: "command with bot mention stripped", | ||
| msg: &tb.Message{Text: "/think@bot hello world"}, | ||
| want: "hello world", | ||
| }, | ||
| { | ||
| name: "no command prefix kept", | ||
| msg: &tb.Message{Text: "hello world"}, | ||
| want: "hello world", | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| got := extractInputFromMessage(tt.msg, trigger) | ||
| assert.Equal(t, tt.want, got) | ||
| }) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
chatWithAgentlogs that it will fall back to the legacy path when the langchaingo model is missing, but it currently returnsnilinstead. This results in no response being sent (and can leave a placeholder message hanging). CallchatWithLegacy(...)here (or at least edit/send an error message) instead of returningnil.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 6ebda7a.
chatWithAgentnow accepts the legacymessagesparameter and callschatWithLegacy(chatCtx, ctx, v2, messages, placeholderMsg)when the langchaingo model is not found.