Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
141 lines
5.1 KiB
Go
141 lines
5.1 KiB
Go
package chatpipeline
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/Tencent/WeKnora/internal/event"
|
|
"github.com/Tencent/WeKnora/internal/types"
|
|
"github.com/Tencent/WeKnora/internal/types/interfaces"
|
|
"github.com/hibiken/asynq"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// stubMemoryService returns a fixed recall so the test can assert what the
|
|
// pipeline does with it, rather than re-testing the memory service.
|
|
type stubMemoryService struct {
|
|
interfaces.MemoryService
|
|
|
|
recall interfaces.MemoryRecall
|
|
lastQuery string
|
|
recallCall int
|
|
}
|
|
|
|
func (s *stubMemoryService) Recall(_ context.Context, query string) interfaces.MemoryRecall {
|
|
s.recallCall++
|
|
s.lastQuery = query
|
|
return s.recall
|
|
}
|
|
|
|
func (s *stubMemoryService) ScheduleExtraction(context.Context, string, string, string) {}
|
|
|
|
func (s *stubMemoryService) Handle(context.Context, *asynq.Task) error { return nil }
|
|
|
|
func newMemoryRecallPlugin(memoryService interfaces.MemoryService) *PluginMemoryRecall {
|
|
return NewPluginMemoryRecall(NewEventManager(), memoryService)
|
|
}
|
|
|
|
// TestMemoryReachesTheMessagesSentToTheModel is the assertion that matters:
|
|
// recalling memory is pointless if it never lands in the request. It walks the
|
|
// recall stage and then the same message assembly the completion plugins use.
|
|
func TestMemoryReachesTheMessagesSentToTheModel(t *testing.T) {
|
|
memoryService := &stubMemoryService{
|
|
recall: interfaces.MemoryRecall{
|
|
Prompt: types.WrapMemoryForPrompt("Preferences:\n- 回答请直接给结论", ""),
|
|
Items: []*types.MemoryItem{
|
|
{ID: "m1", Kind: types.MemoryKindPreference, Content: "回答请直接给结论"},
|
|
},
|
|
},
|
|
}
|
|
plugin := newMemoryRecallPlugin(memoryService)
|
|
|
|
chatManage := &types.ChatManage{}
|
|
chatManage.Query = "帮我看看这个报错"
|
|
chatManage.UserContent = "帮我看看这个报错"
|
|
chatManage.SummaryConfig.Prompt = "你是一个助手。"
|
|
|
|
nextCalled := false
|
|
err := plugin.OnEvent(t.Context(), types.MEMORY_RECALL, chatManage, func() *PluginError {
|
|
nextCalled = true
|
|
return nil
|
|
})
|
|
require.Nil(t, err)
|
|
require.True(t, nextCalled, "the recall stage must never stop the pipeline")
|
|
require.Equal(t, "帮我看看这个报错", memoryService.lastQuery)
|
|
|
|
messages := prepareMessagesWithHistory(chatManage)
|
|
require.NotEmpty(t, messages)
|
|
require.Equal(t, "system", messages[0].Role)
|
|
require.Contains(t, messages[0].Content, "回答请直接给结论",
|
|
"the recalled memory must be present in the system message")
|
|
require.Contains(t, messages[0].Content, "<user_memory>")
|
|
require.True(t, strings.HasPrefix(messages[0].Content, "你是一个助手。"),
|
|
"memory must be appended after the configured prompt, not replace it")
|
|
}
|
|
|
|
func TestMemoryIsAbsentWhenNothingRecalled(t *testing.T) {
|
|
plugin := newMemoryRecallPlugin(&stubMemoryService{})
|
|
|
|
chatManage := &types.ChatManage{}
|
|
chatManage.Query = "随便问点什么"
|
|
chatManage.SummaryConfig.Prompt = "你是一个助手。"
|
|
|
|
require.Nil(t, plugin.OnEvent(t.Context(), types.MEMORY_RECALL, chatManage, func() *PluginError { return nil }))
|
|
require.Empty(t, chatManage.MemoryPrompt)
|
|
require.Empty(t, chatManage.UsedMemories)
|
|
|
|
messages := prepareMessagesWithHistory(chatManage)
|
|
require.NotContains(t, messages[0].Content, "<user_memory>")
|
|
}
|
|
|
|
func TestMemoryRecallEmitsWhatTheAnswerSaw(t *testing.T) {
|
|
memoryService := &stubMemoryService{
|
|
recall: interfaces.MemoryRecall{
|
|
Prompt: types.WrapMemoryForPrompt("About the user:\n- 在做医疗影像", ""),
|
|
Items: []*types.MemoryItem{
|
|
{ID: "m1", Kind: types.MemoryKindProfile, Content: "在做医疗影像"},
|
|
},
|
|
},
|
|
}
|
|
plugin := newMemoryRecallPlugin(memoryService)
|
|
|
|
bus := event.NewEventBus()
|
|
var received types.UsedMemories
|
|
bus.On(event.EventMemoryRecalled, func(_ context.Context, evt event.Event) error {
|
|
data, ok := evt.Data.(event.MemoryRecalledData)
|
|
require.True(t, ok)
|
|
received, ok = data.Memories.(types.UsedMemories)
|
|
require.True(t, ok)
|
|
return nil
|
|
})
|
|
|
|
chatManage := &types.ChatManage{}
|
|
chatManage.Query = "继续上次的事"
|
|
chatManage.EventBus = bus.AsEventBusInterface()
|
|
|
|
require.Nil(t, plugin.OnEvent(t.Context(), types.MEMORY_RECALL, chatManage, func() *PluginError { return nil }))
|
|
|
|
// The chat UI promises "these are the memories this answer saw", so the
|
|
// streamed list has to be the same one that was injected.
|
|
require.Len(t, received, 1)
|
|
require.Equal(t, "m1", received[0].ID)
|
|
require.Equal(t, "在做医疗影像", received[0].Content)
|
|
require.Equal(t, received, chatManage.UsedMemories)
|
|
}
|
|
|
|
func TestMemoryRecallToleratesNoService(t *testing.T) {
|
|
plugin := newMemoryRecallPlugin(nil)
|
|
chatManage := &types.ChatManage{}
|
|
chatManage.SummaryConfig.Prompt = "你是一个助手。"
|
|
require.Nil(t, plugin.OnEvent(t.Context(), types.MEMORY_RECALL, chatManage, func() *PluginError { return nil }))
|
|
require.Empty(t, chatManage.MemoryPrompt)
|
|
}
|
|
|
|
func TestMemoryRecallStageIsRegisteredInThePipeline(t *testing.T) {
|
|
// A stage nobody runs is the failure mode this whole feature has had
|
|
// before, so assert the plugin declares the event the assembler adds.
|
|
plugin := newMemoryRecallPlugin(&stubMemoryService{})
|
|
require.Equal(t, []types.EventType{types.MEMORY_RECALL}, plugin.ActivationEvents())
|
|
}
|