feat: harden support, localize knowledge and add operator availability [skip ci]
Source-only publication of reviewed commit 64e96a7f423673dc29ad56485be9fc79bce8af1c.
Passed in isolation: focused frontend/backend tests, race checks, TypeScript typecheck, lint, Go compilation, Flowgram build, and deterministic SDK build/tests.
Not accepted as a production release: Next.js export/native assembly remains blocked by environment memory limits; browser interaction and MySQL staging acceptance remain unverified. No production server, database/schema, runtime configuration, deployment, release tag, or workflow execution was performed.
Expected source tree: 0a4a7528e9.
This commit is contained in:
175
internal/ai/rag/localization_test.go
Normal file
175
internal/ai/rag/localization_test.go
Normal file
@@ -0,0 +1,175 @@
|
||||
package rag
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"agent-desk/internal/ai"
|
||||
"agent-desk/internal/models"
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/enums"
|
||||
"agent-desk/internal/pkg/i18nx"
|
||||
)
|
||||
|
||||
func TestKnowledgeAnswerLocalizedFallbackPaths(t *testing.T) {
|
||||
tests := []struct{ locale, fallback, retry, noAnswerStatus string }{
|
||||
{i18nx.LocaleEnUS, "The knowledge base currently has no clear information.", "The knowledge base did not contain enough clear information. Try asking a more specific question.", "No answer"},
|
||||
{i18nx.LocaleRuRU, "В базе знаний пока нет однозначной информации.", "В базе знаний недостаточно однозначной информации. Попробуйте задать более конкретный вопрос.", "Нет ответа"},
|
||||
{i18nx.LocaleZhCN, "当前知识库暂无明确信息。", "当前知识库里没有找到足够明确的信息,你可以换个更具体的问法再试一次。", "无答案"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.locale, func(t *testing.T) {
|
||||
ctx := i18nx.WithLocale(context.Background(), tt.locale)
|
||||
called := false
|
||||
failedChat := func(context.Context, string, string) (*ai.ChatCompletionResult, error) {
|
||||
called = true
|
||||
return nil, errors.New("provider failed")
|
||||
}
|
||||
noHits, err := generateKnowledgeAnswer(ctx, "问题 не переводить", nil, enums.KnowledgeAnswerModeStrict, enums.AIAgentFallbackModeNoAnswer, failedChat)
|
||||
if err != nil || called || noHits.Status != enums.KnowledgeAnswerStatusNoAnswer || noHits.Content != tt.fallback {
|
||||
t.Fatalf("no-hit result = %#v, %v; model called = %v", noHits, err, called)
|
||||
}
|
||||
if got := getAnswerStatusName(ctx, noHits.Status); got != tt.noAnswerStatus {
|
||||
t.Fatalf("status = %q", got)
|
||||
}
|
||||
hits := []RetrieveResult{{DocumentID: 1, Content: "Reference content"}}
|
||||
failed, err := generateKnowledgeAnswer(ctx, "Q", hits, enums.KnowledgeAnswerModeAssist, enums.AIAgentFallbackModeNoAnswer, failedChat)
|
||||
if err != nil || !called || failed.Status != enums.KnowledgeAnswerStatusFallback || failed.Content != tt.fallback {
|
||||
t.Fatalf("failed model result = %#v, %v", failed, err)
|
||||
}
|
||||
for _, empty := range []*ai.ChatCompletionResult{nil, {}, {Content: " \n\t"}} {
|
||||
got, err := generateKnowledgeAnswer(ctx, "Q", hits, enums.KnowledgeAnswerModeStrict, enums.AIAgentFallbackModeSuggestRetry, func(context.Context, string, string) (*ai.ChatCompletionResult, error) { return empty, nil })
|
||||
if err != nil || got.Status != enums.KnowledgeAnswerStatusFallback || got.Content != tt.retry {
|
||||
t.Fatalf("empty result fallback = %#v, %v", got, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestKnowledgeGenerationPreservesUserAndModelContent(t *testing.T) {
|
||||
for _, locale := range []string{i18nx.LocaleEnUS, i18nx.LocaleRuRU, i18nx.LocaleZhCN} {
|
||||
ctx := i18nx.WithLocale(context.Background(), locale)
|
||||
question := " 问题:User's question\n回答:not a wrapper "
|
||||
body := " 回答:body\n相似问:literal user text\n "
|
||||
output := &ai.ChatCompletionResult{Content: " 模型回答\n", ModelName: "test-model", PromptTokens: 4, CompletionTokens: 2}
|
||||
got, err := generateKnowledgeAnswer(ctx, question, []RetrieveResult{{DocumentID: 1, DocumentTitle: "用户标题", Content: body}}, enums.KnowledgeAnswerModeStrict, enums.AIAgentFallbackModeNoAnswer, func(modelCtx context.Context, systemPrompt, userPrompt string) (*ai.ChatCompletionResult, error) {
|
||||
if i18nx.LocaleFromContext(modelCtx) != locale {
|
||||
t.Fatal("locale not forwarded to model")
|
||||
}
|
||||
if systemPrompt != i18nx.Getf(locale, "knowledge.rag.system.strict") || !strings.Contains(userPrompt, question) || !strings.Contains(userPrompt, body) {
|
||||
t.Fatalf("incorrect prompts: %q, %q", systemPrompt, userPrompt)
|
||||
}
|
||||
return output, nil
|
||||
})
|
||||
if err != nil || got.Status != enums.KnowledgeAnswerStatusNormal || got.ChatCompletionResult != *output {
|
||||
t.Fatalf("model output was changed: %#v, %v", got, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestKnowledgeCancellationBeforePersistenceOrProviderCalls(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(i18nx.WithLocale(context.Background(), i18nx.LocaleRuRU))
|
||||
cancel()
|
||||
if _, err := Answer.DebugSearch(ctx, request.KnowledgeSearchRequest{Question: "Q"}); !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("debug search ignored cancellation: %v", err)
|
||||
}
|
||||
if _, err := Answer.DebugAnswer(ctx, request.KnowledgeAnswerRequest{Question: "Q"}, nil); !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("debug answer ignored cancellation: %v", err)
|
||||
}
|
||||
if err := Index.IndexDocumentByID(ctx, 1); !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("document build ignored cancellation: %v", err)
|
||||
}
|
||||
if err := Index.IndexFAQByID(ctx, 1); !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("FAQ build ignored cancellation: %v", err)
|
||||
}
|
||||
if _, err := Retrieve.Retrieve(ctx, RetrieveRequest{Query: "Q"}); !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("retrieval ignored cancellation: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKnowledgeGenerationCancellationIsNotFallback(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(i18nx.WithLocale(context.Background(), i18nx.LocaleRuRU))
|
||||
defer cancel()
|
||||
got, err := generateKnowledgeAnswer(ctx, "Q", []RetrieveResult{{DocumentID: 1, Content: "source"}}, enums.KnowledgeAnswerModeStrict, enums.AIAgentFallbackModeNoAnswer, func(modelCtx context.Context, _, _ string) (*ai.ChatCompletionResult, error) {
|
||||
cancel()
|
||||
return nil, modelCtx.Err()
|
||||
})
|
||||
if got != nil || !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("cancelled model should not become a saved fallback: %#v, %v", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFAQGeneratedWrappersPreserveSourceContent(t *testing.T) {
|
||||
faq := models.KnowledgeFAQ{
|
||||
Question: " 问题:What?\n回答:part of question ",
|
||||
SimilarQuestions: `["相似问:literal; text", "", " another\n问题:line "]`,
|
||||
Answer: " 回答:user answer\n相似问:inside answer\n问题:last line ",
|
||||
}
|
||||
original := faq
|
||||
source := sourceFAQContent(faq)
|
||||
for _, locale := range []string{i18nx.LocaleEnUS, i18nx.LocaleRuRU, i18nx.LocaleZhCN} {
|
||||
ctx := i18nx.WithLocale(context.Background(), locale)
|
||||
expected := source.render(locale)
|
||||
for _, stored := range []string{source.legacyChinese(), source.render(i18nx.LocaleEnUS), source.render(i18nx.LocaleRuRU), source.render(i18nx.LocaleZhCN)} {
|
||||
got := renderVerifiedFAQChunkContent(ctx, stored, faq)
|
||||
if got != expected || !strings.Contains(got, faq.Question) || !strings.Contains(got, faq.Answer) || !strings.Contains(got, " another\n问题:line ") {
|
||||
t.Fatalf("%s changed source text: %q", locale, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
if faq != original {
|
||||
t.Fatal("source model mutated")
|
||||
}
|
||||
if got := buildFAQChunkContent(faq); got != source.render(i18nx.DefaultLocale) {
|
||||
t.Fatalf("index did not use configured locale: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFAQUnknownAndStaleDerivedContentStaysRecorded(t *testing.T) {
|
||||
faq := models.KnowledgeFAQ{Question: "Current Q", Answer: "Current A"}
|
||||
ctx := i18nx.WithLocale(context.Background(), i18nx.LocaleRuRU)
|
||||
for _, stored := range []string{
|
||||
"问题:Old Q\n回答:Historical A",
|
||||
"问题:Current Q\n回答:Current A\n",
|
||||
"问题:Current Q\n回答:User-authored custom answer",
|
||||
"相似问:not a generated chunk",
|
||||
"{malformed:derived}",
|
||||
"",
|
||||
} {
|
||||
if got := renderVerifiedFAQChunkContent(ctx, stored, faq); got != stored {
|
||||
t.Fatalf("unverified stored content was rewritten: %q => %q", stored, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextWrappersAreLocalizedWithoutChangingSource(t *testing.T) {
|
||||
content := " 原始文档\n回答:literal\n "
|
||||
tests := []struct{ locale, label string }{
|
||||
{i18nx.LocaleEnUS, "[Document: Document#7 | Section: 用户章节]"},
|
||||
{i18nx.LocaleRuRU, "[Документ: Документ №7 | Раздел: 用户章节]"},
|
||||
{i18nx.LocaleZhCN, "【文档:文档#7|章节:用户章节】"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
ctx := i18nx.WithLocale(context.Background(), tt.locale)
|
||||
got := Retrieve.BuildContext(ctx, []RetrieveResult{{DocumentID: 7, SectionPath: "用户章节", Content: content}}, 4000)
|
||||
if got != tt.label+"\n"+content+"\n\n" {
|
||||
t.Fatalf("%s unexpected context: %q", tt.locale, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestKnowledgeGenerationDefaultLocaleAndEmptyResponseMetrics(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
noHits, err := generateKnowledgeAnswer(ctx, "Q", nil, enums.KnowledgeAnswerModeStrict, enums.AIAgentFallbackModeNoAnswer, nil)
|
||||
if err != nil || noHits.Content != i18nx.Get("knowledge.rag.fallback.noAnswer") {
|
||||
t.Fatalf("default-locale caller changed behavior: %#v, %v", noHits, err)
|
||||
}
|
||||
empty := &ai.ChatCompletionResult{Content: " \n", ModelName: "local-test-model", PromptTokens: 12, CompletionTokens: 3}
|
||||
got, err := generateKnowledgeAnswer(ctx, "Q", []RetrieveResult{{DocumentID: 1, Content: "reference"}}, enums.KnowledgeAnswerModeStrict, enums.AIAgentFallbackModeNoAnswer, func(context.Context, string, string) (*ai.ChatCompletionResult, error) { return empty, nil })
|
||||
if err != nil || got.Status != enums.KnowledgeAnswerStatusFallback || got.Content != noHits.Content || got.ModelName != empty.ModelName || got.PromptTokens != empty.PromptTokens || got.CompletionTokens != empty.CompletionTokens {
|
||||
t.Fatalf("empty completion must retain usage metrics: %#v, %v", got, err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user