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:
82
internal/ai/rag/faq_content.go
Normal file
82
internal/ai/rag/faq_content.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package rag
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"agent-desk/internal/models"
|
||||
"agent-desk/internal/pkg/i18nx"
|
||||
)
|
||||
|
||||
// faqContent keeps application-owned labels separate from source text. Never
|
||||
// translate, trim, or replace substrings in the source question or answer.
|
||||
type faqContent struct {
|
||||
question string
|
||||
similarQuestions []string
|
||||
answer string
|
||||
}
|
||||
|
||||
func sourceFAQContent(faq models.KnowledgeFAQ) faqContent {
|
||||
content := faqContent{question: faq.Question, answer: faq.Answer}
|
||||
if faq.SimilarQuestions != "" {
|
||||
_ = json.Unmarshal([]byte(faq.SimilarQuestions), &content.similarQuestions)
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
func (content faqContent) render(locale string) string {
|
||||
parts := []string{i18nx.Getf(locale, "knowledge.rag.faq.question", content.question)}
|
||||
if len(content.similarQuestions) > 0 {
|
||||
similar := joinNonEmptySimilarQuestions(content.similarQuestions, i18nx.Getf(locale, "knowledge.rag.faq.separator"))
|
||||
parts = append(parts, i18nx.Getf(locale, "knowledge.rag.faq.similarQuestions", similar))
|
||||
}
|
||||
parts = append(parts, i18nx.Getf(locale, "knowledge.rag.faq.answer", content.answer))
|
||||
return strings.Join(parts, "\n")
|
||||
}
|
||||
|
||||
// Index generation deliberately uses the configured default, not the locale of
|
||||
// whichever administrator requested a build. A locale read never re-embeds data.
|
||||
func buildFAQChunkContent(faq models.KnowledgeFAQ) string {
|
||||
return sourceFAQContent(faq).render(i18nx.DefaultLocale)
|
||||
}
|
||||
|
||||
// legacyChinese renders the exact pre-localization serialization. This is a
|
||||
// recognition format, not UI copy. Keep it frozen when catalog wording changes.
|
||||
func (content faqContent) legacyChinese() string {
|
||||
parts := []string{fmt.Sprintf("问题:%s", content.question)}
|
||||
if len(content.similarQuestions) > 0 {
|
||||
parts = append(parts, "相似问:"+joinNonEmptySimilarQuestions(content.similarQuestions, ";"))
|
||||
}
|
||||
parts = append(parts, "回答:"+content.answer)
|
||||
return strings.Join(parts, "\n")
|
||||
}
|
||||
|
||||
// Only a full byte-for-byte match against a generated representation of the
|
||||
// current source proves which text belongs to us. Stale/manual/malformed chunks
|
||||
// stay as recorded. In particular, do not infer labels by splitting on Chinese
|
||||
// words: the source content may itself contain those words or arbitrary lines.
|
||||
// This function is for live retrieval only, never historical log replay.
|
||||
func renderVerifiedFAQChunkContent(ctx context.Context, stored string, faq models.KnowledgeFAQ) string {
|
||||
content := sourceFAQContent(faq)
|
||||
if stored == content.legacyChinese() {
|
||||
return content.render(i18nx.LocaleFromContext(ctx))
|
||||
}
|
||||
for _, locale := range []string{i18nx.LocaleEnUS, i18nx.LocaleRuRU, i18nx.LocaleZhCN} {
|
||||
if stored == content.render(locale) {
|
||||
return content.render(i18nx.LocaleFromContext(ctx))
|
||||
}
|
||||
}
|
||||
return stored
|
||||
}
|
||||
|
||||
func joinNonEmptySimilarQuestions(items []string, separator string) string {
|
||||
nonEmpty := make([]string, 0, len(items))
|
||||
for _, item := range items {
|
||||
if item != "" {
|
||||
nonEmpty = append(nonEmpty, item)
|
||||
}
|
||||
}
|
||||
return strings.Join(nonEmpty, separator)
|
||||
}
|
||||
Reference in New Issue
Block a user